To define columns in Contember, you can add properties to your entity class. Each property should be defined using a column definition method that specifies the data type for the column.

Example how to define columns for a Post entity:

export class Post {
	title = c.stringColumn().notNull()
	publishedAt = c.dateTimeColumn()
}

In this example, the Post entity has two columns: title and publishedAt. The title column is a string column that is defined as not nullable, while the publishedAt column is a date-time column.

Supported data types

Contember supports several different data types for columns, including string, int, double, numeric, bool, dateTime, date, json, and uuid. You can use the following methods to define columns of different types:

Contember TypeDefinition methodPostgreSQL typeDescription
StringstringColumntextGeneric text field with arbitrary length.
IntintColumnintegerStores whole signed numbers (32b default)
DoubledoubleColumndouble precisionFloating point numbers according to IEEE 64-bit float.
NumericnumericColumnnumeric(p, s)Exact decimal numbers with a fixed precision and scale (e.g. money). Transferred as a string. See more in a section below.
BoolboolColumnbooleanBinary true/false value.
DateTimedateTimeColumntimestamptzFor storing date and time, converted to UTC by default and transferred in ISO 8601 format (e.g. 2032-01-18T13:36:45Z). The precision and exact format of the serialized value can be tuned — see DateTime serialization.
DatedateColumndateDate field without a time part. It's transferred in YYYY-MM-DD format (e.g. 2032-01-18).
JsonjsonColumnjsonbStores arbitrary JSON.
UUIDuuidColumnuuidUniversally unique identifier, used for all primary keys by default.
EnumenumColumncustom domainField with predefined set of possible values. See more in a section below.
Note

The type of column in PostgreSQL database can be changed using .columnType(...) in schema definition.

Example: changing database type of Json column

export class Post {
	config = c.jsonColumn().columnType('json')
}

Numeric (exact decimals)

The numericColumn method defines an exact decimal column backed by the PostgreSQL numeric(precision, scale) type. Unlike doubleColumn, which uses IEEE 64-bit floating point and is subject to rounding errors, numeric stores the value exactly. Use it whenever you need precise arithmetic — most importantly for money, but also for quantities, rates, measurements, and any value where a rounding error is unacceptable.

numericColumn takes two required arguments:

  • precision — the total number of significant digits.
  • scale — the number of digits after the decimal point.

Example how to define a price column:

export class Product {
	price = c.numericColumn(20, 9).notNull()
}

This maps to a numeric(20, 9) column in PostgreSQL, allowing up to 20 significant digits with 9 of them after the decimal point.

Transferred as a string

Numeric values are transported over GraphQL as strings rather than numbers. This is intentional: JavaScript numbers are IEEE 64-bit floats and cannot represent every decimal value exactly (for example, 0.1 + 0.2 !== 0.3), so passing them through a GraphQL Float would silently corrupt the very precision a numeric column exists to protect. The column is therefore exposed through a custom Decimal GraphQL scalar, and the generated TypeScript SDK types it as string.

On input, the Decimal scalar accepts either a string (e.g. "123.45", including scientific notation like "1.5e10") or a plain JSON number, which is normalized to a string. Values that are not valid decimals — as well as NaN and Infinity — are rejected.

On output, the API returns the exact PostgreSQL numeric text representation. This preserves trailing zeros up to the column scale, so a value of 123.45 stored in a numeric(20, 9) column is returned as "123.450000000".

mutation {
  createProduct(data: { price: "123.45" }) {
    node {
      price # => "123.450000000"
    }
  }
}

Filtering and ordering work the same way as for other numeric types — operators such as eq, gt, lt, in, and notIn are available, and comparison happens in the database as exact numeric arithmetic. Filter values are also passed as strings:

query {
  listProduct(filter: { price: { gt: "100.5" } }, orderBy: [{ price: asc }]) {
    price
  }
}
Tip

If you don't need exact precision and prefer working with plain JavaScript numbers, use doubleColumn instead.

Column flags and options

In addition to defining the data type for a column, you can also specify additional flags such as nullability and uniqueness.

Not null fields

By default, columns are nullable, meaning that they can store a null value. However, you can specify that a column is not nullable by calling the .notNull() method on the column definition.

Example how to define a not-null string column:

title = c.stringColumn().notNull()

In this example, the title column is a string column that is defined as not nullable. This means that you must provide a value for the title column when you create a record in the Post entity.

Unique fields

You can mark a column as unique by calling the .unique() method on it:

slug = c.stringColumn().unique()

You can also define composite unique keys by using a class decorator:

@c.Unique("locale", "slug")
export class Post {
	slug = c.stringColumn().notNull()
	locale = c.stringColumn().notNull()
}

Unique constraints with options

You can specify additional options for unique constraints:

// Basic unique constraint with timing options
@c.Unique({
	fields: ["locale", "slug"],
	timing: "deferrable"
})
export class Post {
	slug = c.stringColumn().notNull()
	locale = c.stringColumn().notNull()
}

// Unique index with custom options
@c.Unique({
	fields: ["email"],
	index: true,
	nulls: "not distinct",
	method: "btree"
})
export class User {
	email = c.stringColumn()
}
Note

PostgreSQL distinguishes between unique constraints and unique indexes, which have different properties that cannot be combined:

  • Unique constraints support timing options (timing: "deferrable" or "deferred") but don't support nulls behavior or index method configuration
  • Unique indexes (when index: true) support nulls behavior ("distinct" or "not distinct") and index methods, but don't support timing options

Choose the appropriate type based on your needs.

Available options for unique constraints:

  • timing: Constraint timing ("deferrable" or "deferred") - only for constraints, not indexes
  • index: Set to true to create a unique index instead of a constraint
  • nulls: For unique indexes only, specify null handling ("distinct" or "not distinct")
  • method: For unique indexes only, specify index method ("btree", "gin", "gist", "hash", "brin", "spgist")
Tip

You can also reference relationships in Unique.

You can then use these unique combinations to fetch a single record. "One has one" relationships are marked as unique by default.

Indexes

To define ordinary non-unique index, you can use Index decorator in your schema definition.

Example how to define a single column index

@c.Index('title')
export class Article {
	title = c.stringColumn()
}

Example how to define a multi column index

@c.Index('title', 'description')
export class Article {
	title = c.stringColumn()
	description = c.stringColumn()
}

Indexes with options

You can specify additional options for indexes:

// Index with custom method
@c.Index({
	fields: ['title'],
	method: 'btree'
})
export class Article {
	title = c.stringColumn()
}

// Multi-column index with options
@c.Index({
	fields: ['category', 'publishedAt'],
	method: 'btree'
})
export class Article {
	category = c.stringColumn()
	publishedAt = c.dateTimeColumn()
}

Available options for indexes:

  • fields: Array of field names to include in the index
  • method: Index method ("btree", "gin", "gist", "hash", "brin", "spgist")
  • opClass: Operator class applied to the indexed columns (e.g. "gin_trgm_ops" for trigram search). Requires the corresponding PostgreSQL extension to be installed.
Tip

Use gin method for JSON columns and array/list columns as it provides efficient indexing for these data types:

@c.Index({ fields: ['metadata'], method: 'gin' })
export class Article {
	metadata = c.jsonColumn()
}

To make the similar and wordSimilar filter operators perform well, add a GIN index with the gin_trgm_ops operator class on the searched column:

@c.Index({ fields: ['title'], method: 'gin', opClass: 'gin_trgm_ops' })
export class Article {
	title = c.stringColumn()
}

This generates CREATE INDEX ... USING gin ("title" gin_trgm_ops).

Requires the pg_trgm extension

The gin_trgm_ops operator class is provided by the PostgreSQL pg_trgm extension. It must be enabled in your database before the migration that creates the index runs, otherwise the migration fails with operator class "gin_trgm_ops" does not exist. Enable it once per database with:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

Changing column name

To change the name of a column in a database, you can use the columnName method on the column definition. By default, Contember will use the "snake case" version of the property name as the column name in the database.

Example how to define a column with a custom column name:

publishedAt = c.dateTimeColumn().columnName('published')

In this example, the publishedAt property is a date-time column that is defined with the column name published. This means that the column will be named published in the database, rather than published_at.

It is worth noting that when working with Contember, you will typically interact with the GraphQL schema rather than the underlying database schema. This means that you will usually use the property names defined in your entity classes to query and manipulate data, rather than the column names in the database. You usually only use database column names in custom views.

You might use the columnName method to maintain backward compatibility when making changes to your schema. For example, if you need to rename a field name in your schema, you can use the columnName method to keep old column name in your database. This can help to minimize the impact of schema changes on your application.

Changing column type

The columnType method allows you to specify the underlying column type in the database for a column in your entity schema. By default, Contember will map the column types in your entity schema to the appropriate column types in the database based on the data type of the property.

However, you can use the columnType method to specify a custom column type in the database for a column. This can be useful if you need to use a column type in the database that is not supported by Contember, or if you need to customize the mapping between the column types in your schema and the database.

Example how to use the columnType method to specify a custom column type in the database:

config = c.jsonColumn().columnType('json')

In this example, the config property is a JSON column that is defined with the column type json in the database. This means that the config column will be of type json in the database, rather than the default jsonb type.

Validating JSON columns with a JSON Schema

The schema method (available only on jsonColumn()) lets you attach a JSON Schema to a JSON column. The schema is stored as metadata in the Contember schema and the value provided on every create / update mutation is validated against it. Validation errors are reported the same way as the built-in field validation rules.

Example how to attach a JSON Schema to a JSON column:

address = c.jsonColumn().schema({
	type: 'object',
	properties: {
		street: { type: 'string' },
		zip: { type: 'string', pattern: '^[0-9]{5}$' },
		country: { type: 'string', enum: ['CZ', 'SK'] },
	},
	required: ['street', 'zip'],
	additionalProperties: false,
})

Notes and limitations:

  • The schema is metadata only — it does not change the database column type and adds no database-level CHECK constraint. Changing the attached schema produces a metadata-only migration with no DDL.
  • Validation runs on input only. The value is validated on every create / update mutation that includes the column. Existing rows are never re-validated — changing (or first adding) a schema does not trigger any migration, backfill, or revalidation of data already stored, so rows written before the change (or written through paths that bypass validation) may not conform to the current schema.
  • The schema is not enforced on the data-transfer / contember import path. Import writes rows with raw INSERT statements that bypass the content API, so imported JSON values are not validated against the attached schema.
  • A schema-level default value (jsonColumn().default(...)) is not validated against the attached schema, because defaults are applied without going through input validation.
  • null values are not validated (use notNull() to forbid them) and a column is only validated when it is present in the mutation input.
  • The attached schema is exposed as part of the project schema metadata (e.g. via the System API schema query) but is not reflected in the Content API GraphQL SDL, nor used to derive a typed shape in the generated client — the column stays typed as JSONValue.
  • The schema method is defined on the shared column builder, but it is only honored for JSON columns; calling it on a non-JSON column (e.g. stringColumn().schema(...)) is silently ignored.
  • The validator implements a pragmatic subset of JSON Schema with no extra runtime dependency. It is not a full draft 2020-12 implementation. Supported keywords: type (including integer and type arrays), enum, const, object (properties, required, additionalProperties, minProperties, maxProperties), array (items, minItems, maxItems, uniqueItems), string (minLength, maxLength, pattern), number (minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf), and the combinators allOf, anyOf, oneOf, not. All other keywords are silently ignored (treated as valid) — including $ref, $defs, format, patternProperties, propertyNames, dependentRequired, dependentSchemas, if / then / else, contains, and the 2020-12 tuple keyword prefixItems. In particular items is applied to all array elements (draft-07 style); 2020-12 tuple validation via prefixItems will not be enforced. Treat anything not in the supported list as documentation rather than an enforced constraint.

Default value

The default method allows you to specify a default value for a column in your entity schema. When a default value is specified, it will be used as the value of the column when a new record is created if no value is explicitly provided.

Example how to use the default method to specify a default value for a column:

published = c.boolColumn().default(false)

In this example, the published property is a boolean column that is defined with the default value false. This means that when a new record is created, the published column will be set to false if no value is explicitly provided.

The default method can be used with any column type that supports default values in the database. For example, you can use the default method with string, integer, double, and boolean columns, as well as with enum and JSON columns.

Changing GraphQL type

The typeAlias method allows you to specify a custom type for a column in the GraphQL schema. By default, Contember will map the column types in your entity schema to the appropriate GraphQL types based on the data type.

However, you can use the typeAlias method to specify a custom GraphQL type for a column. This can be useful if you need to customize the mapping between the column types in your schema and the GraphQL types.

Example how to use the typeAlias method to specify a custom GraphQL type for a column:

publishedAt = c.dateTimeColumn().typeAlias('CustomDateTime')

In this example, the publishedAt property is a date-time column that is mapped to the CustomDateTime GraphQL type in the schema. This means that the publishedAt column will be of type CustomDateTime in the GraphQL schema, rather than the default DateTime type.

Sequences

The sequence method allows you to enable a generated sequence on an integer column that is backed by a PostgreSQL identity column. This can be useful for generating unique, incrementing values for a column in your entity.

Example how to enable a sequence:

export class Task {
	counter = c.intColumn().sequence().notNull()
}
Caution

Sequence column cannot be nullable.

You can also pass an optional configuration with start and precedence. The start property allows you to specify the starting value for the sequence, and the precedence property allows you to specify whether the generated value should always (value ALWAYS) be used, or only if no value has been specified for the column (value BY DEFAULT, this is implicit behaviour).

Example of how to enable a sequence with different start and precedence:

export class Task {
	counter = c.intColumn().notNull().sequence({ start: 1000, precedence: 'ALWAYS' })
}

In this example, the counter column in the Task entity is defined as an integer column with a generated sequence. The sequence will start at value 1000, and the generated value will always be used, regardless of whether a value has been specified for the column.

Enums

Enums in Contember allow you to define a set of predefined values for a column in your entity schema. Enums can be used to limit the possible values that can be stored in a column, and can be useful for defining values that are used consistently throughout your application. The enum defined in a schema is mapped to a GraphQL enum.

To define an enum, you can use the createEnum method. This method takes a list of string values, which will become the possible values of the enum.

Example how to define an enum for a status column in a Task entity:

export const TaskStatus = c.createEnum('pending', 'in_progress', 'completed')

export class Task {
  status = c.enumColumn(TaskStatus)
}

Single enum defined using the createEnum method can be used in multiple columns or entities. Y

You can also use methods like setNull or unique.