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.
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.