Skip to content
Talk to our solutions team

Embeddables

An embeddable is a named, entity-shaped group of fields with no table of its own, a field that references one stores the whole structure inside a single column. The column type is not implied by the embeddable, see Storage.

Use an embeddable when a structure repeats across entities and you never need to query, index or join its parts: an address block, a money amount, a contact block.

embeddables:
- name: pincode
description: Postal code block
fields:
- name: code
type: string
required: true
- name: address
description: Reusable address structure
fields:
- name: line1
type: string
required: true
- name: line2
type: string
nullable: true
- name: city
type: string
required: true
- name: country
type: string
required: true
- name: pincode
type: pincode
KeyTypeRequiredMeaning
embeddables[].namestringyesThe name a field references, by type: or embeddable:.
embeddables[].descriptionstringnoFree text. Kept on the definition; not exported anywhere.
embeddables[].fields[]listyesField definitions, same YAML shape as entity fields.

A sub-field with no name: is dropped and the load continues. No error, no diagnostic.

Embeddables are declared per datastore, alongside entities:, and can sit in any *.yaml or *.yml file under the datastore folder.

Two spellings, one result:

entities:
- name: person
inherits: kisai.id
fields:
- name: firstname
type: string
- name: home
type: address # the embeddable's name used as the field type
- name: work
embeddable: address # explicit key
nullable: true
KeyEffect
fields[].typeAn unrecognised type name is kept as a type reference, an embeddable or a custom type.
fields[].embeddableOverwrites the field’s type name with the named embeddable. It does not change the resolved base type: with both keys present the column type still comes from type:.
fields[].subfieldsParsed by the YAML shape and then dropped. Declaring sub-fields inline does nothing.

Nothing checks that the name resolves, a field typed addres loads cleanly and becomes an ordinary string column.

The parent column is NOT NULL unless the parent field carries nullable: true. required: and nullable: on the sub-fields have no effect on the emitted DDL.

The embeddable declaration alone does not produce a JSON column. Nothing in the type resolver maps an embeddable name to a JSON type, the field falls through as an unresolved type reference and is narrowed to string.

Field declarationPostgreSQLSQLiteDuckDBClickHouse
type: <embeddable> on its ownVARCHAR(255)TEXTVARCHAR(255)String
type: <embeddable> plus a matching customtypes: entryJSONBTEXTJSONString
type: jsonb (aliases json, object)JSONBTEXTJSONString

To get a JSON column, declare a custom type of the same name. kind: defaults to composite, and a composite type resolves to the backend’s JSON type:

embeddables:
- name: address
fields:
- name: line1
type: string
- name: city
type: string
customtypes:
- name: address

The customtypes: entry decides the column type; the embeddables: entry documents the shape. See Types for the full custom-type vocabulary.

A structured defaultvalue:, a YAML map or list, produces no DEFAULT clause at all: the DDL writer treats map, slice and struct defaults as non-literals and skips them, a string defaultvalue: such as "{}" is emitted as a bare SQL literal (DEFAULT '{}'); the explicit ::jsonb cast is added only when the field’s resolved type is jsonb or array, which an embeddable-typed field is not.

An embeddable is not a nested entity. The engine treats the column as one opaque value: it is written whole and read whole. Sub-field declarations parse, and almost none of them do anything.

Declared on a sub-fieldEffect at runtime
validations:None. The validation hook walks the entity’s own fields only.
defaultvalue:None. Defaults are applied per entity field.
computed: / compute:None. Computed fields are resolved per entity field.
references:None. No foreign key is emitted for a sub-field.
enum:None. A sub-field creates no reference to the enum’s table; the embedded document is one JSON column.
compliances:Read by a masking hook that data.svc does not register. No effect on responses.
type:Recorded on the definition; used by nothing at runtime.

Compliance annotations behave the same way for the block’s own service: masking, redaction and hiding of embeddable sub-fields is implemented, but the hook that applies it is not in the chain that data.svc builds. Do not rely on an embeddable to hide a sub-field. Use access rules on the entity instead.

An embeddable may name another embeddable as a sub-field type, as address.pincode does above. The reference is recorded on the definition and nothing resolves it, nesting is documentation, not enforcement, and it has no effect on the emitted column.

The value is a single column. Filters in the query DSL address that column, not its contents.

ExpressionResult
home = <value>, $eq, $neCompares the whole column value.
$contains with a JSON string needleCompiles to "home" @> $1 on PostgreSQL: containment against the JSON value.
$contains with a JSON object needleRejected by the decoder: invalid where clause: field "home": unknown value type.
home.city = "Pune"Compiles to WHERE "home.city" = $1, the whole dotted path is quoted as one identifier, and PostgreSQL rejects the column.

The containment form is the only route into the stored structure, and only against a JSON-typed column:

{
"from": "person",
"where": { "home": { "$contains": "{\"city\":\"Pune\"}" } }
}

POST /rest/:entity/search takes an equality-only body filter ({"filter": {"field": "value"}}), so it cannot reach sub-fields either, the full HTTP surface is at Data API reference.

The value comes back in whatever shape the driver produces for that column:

  • a parsed object,
  • raw JSON text,
  • on PostgreSQL’s JSONB scan path, a one-element array wrapping the object.

The data is preserved in every case. A client that reads embeddables must tolerate all three.

Embeddables merge by name across every *.yaml / *.yml under the datastore folder, walked recursively in lexical order. The last file wins, and it replaces the definition wholesale: no field-level merge, and no diagnostic. This differs from entities and traits, which keep the first definition and log the duplicate. Renaming a file can change which definition survives.

The schema exports describe an embeddable-typed field by its underlying type, a plain string, whether or not a composite custom type gives it a JSON column. The embeddable is not emitted as a named object type in JSON Schema, Zod, CUE, OpenAPI or the GraphQL SDL.

// GET /schema/zod/person
export const PersonSchema = z.object({
id: z.string().ulid(),
home: z.string()...
});

Generated clients therefore type the field as a string. Treat the embeddable’s shape as a contract you enforce yourself.