Skip to content
Settings

Entity model export

A model export returns the full structural model of an entity type in a compact JSON form. It carries every node, every field and every declared type.

Two converters render the model. SIMPLE_VIEW is Cyoda’s native path-keyed form. JSON_SCHEMA is a JSON Schema document. This page is the wire-format specification for both.

For the concepts behind a model, and for discovery, widening and locking, see Modeling entities and Entities and lifecycle.

GET /model/export/{converter}/{entityName}/{modelVersion}
Parameter Type Description
converter String SIMPLE_VIEW or JSON_SCHEMA
entityName String Name of the entity model
modelVersion Integer Version number of the entity model

Response Content-Type: application/json

Both converters return the same envelope. currentState and model are always present; uniqueKeys appears only when the model declares composite unique keys.

Key Type Description
currentState String Lifecycle state of the model: "UNLOCKED" or "LOCKED"
model Object The exported model, in the converter’s own form
uniqueKeys Array Composite unique-key definitions; omitted when none are declared
{
"currentState": "LOCKED",
"model": { "…": "…" },
"uniqueKeys": [
{ "id": "by-email", "fields": ["$.email"] },
{ "id": "by-org-and-handle", "fields": ["$.org", "$.handle"] }
]
}

The model value is a flat JSON object. Its keys are node paths, and its values are node descriptors. This single-depth map holds the full nested tree. Each descriptor is an object, and it describes one structural level.

A node path uses JSONPath syntax rooted at $. It is the same path that a search condition uses to address the same place.

Path Meaning
$ The root object
$.address A nested object field
$.laureates[*] The elements of the laureates array on the root object
$.orders[*].lines[*] The elements of lines, nested inside orders array elements

Write one [*] for each array level. The server sorts the node paths lexicographically. Every export contains the root node $.

A node descriptor is a JSON object with two kinds of entry.

Data fields — keys starting with . carry the field’s type descriptor:

{
".category": "STRING",
".year": "YEAR",
".score": "DOUBLE"
}

An array field’s key carries one [*] per array level, and the value is the type of the element at that depth:

{
".tags[*]": "STRING",
".matrix[*][*]": "INTEGER"
}

An array level whose elements were never observed declares the level, but not the element type. Its value is "NULL":

{ ".e[*]": "NULL" }

Structural fields — keys starting with # mark this node’s role. Only two values are emitted:

Key Value Meaning
# "ARRAY_ELEMENT" This node describes the elements of its parent array
#.fieldName "OBJECT" fieldName carries object structure and has its own node

A nested object is not inlined into its parent with a dotted key. It gets a #.fieldName marker in the parent and a node of its own:

{
"$": { "#.address": "OBJECT", ".name": "STRING" },
"$.address": { ".city": "STRING", ".zip": "STRING" }
}

The server sorts the keys inside a node. Structural keys (#) come before data keys (.), because # precedes . in ASCII.

A field observed in more than one kind declares each kind, and the model enforces each one. The export therefore names each branch.

A field seen as both a string and an object carries a data entry and a structural marker. The object branch gets a node of its own:

{
"$": { "#.o": "OBJECT", ".o": "STRING" },
"$.o": { ".a": "INTEGER" }
}

A field seen as both a scalar and an array names both forms in one node:

{ ".poly": "STRING", ".poly[*]": "STRING" }

A type descriptor is a JSON string: either one DataType name, or a bracketed set when the field declares several.

Monomorphic:

"STRING"

Polymorphic:

"[INTEGER, STRING]"

Types inside the brackets are sorted by the DataType enum ordering: the integer families, then the decimal families, then text, temporal, identifier, binary, boolean, and NULL last.

Twenty types, and no others:

Group DataType Description
Integer INTEGER 32-bit signed integer
LONG 64-bit signed integer
BIG_INTEGER Arbitrary-precision integer fitting Int128
UNBOUND_INTEGER Integer of arbitrary magnitude
Decimal DOUBLE Decimal within the precision-15, scale-292 envelope
BIG_DECIMAL Decimal fitting Trino’s fixed-scale Int128 encoding
UNBOUND_DECIMAL Decimal of arbitrary precision and scale
Text STRING Variable-length character sequence
CHARACTER A single Unicode character
Temporal LOCAL_DATE Date without time zone
LOCAL_DATE_TIME Date-time without time zone
LOCAL_TIME Time without date
ZONED_DATE_TIME Date-time with time zone
YEAR Year value
YEAR_MONTH Year and month
Identifier UUID_TYPE UUID
TIME_UUID_TYPE Version 1 (time-based) UUID
Binary BYTE_ARRAY Binary data, base64-encoded
Boolean BOOLEAN Boolean value
Null NULL No type observed at this leaf

Flat object with an array of objects. In this example "2020" classifies as YEAR, not STRING. A date-shaped sample value gives the field a temporal type at registration.

// posted: {"category":"chemistry","year":"2020",
// "laureates":[{"firstname":"Emmanuelle","surname":"Charpentier","id":"991"}]}
{
"currentState": "UNLOCKED",
"model": {
"$": {
"#.laureates": "OBJECT",
".category": "STRING",
".year": "YEAR"
},
"$.laureates[*]": {
"#": "ARRAY_ELEMENT",
".firstname": "STRING",
".id": "STRING",
".surname": "STRING"
}
}
}

Nested object and a primitive array. The array’s element type is named once; its length is not part of the model.

// posted: {"name":"Alice","scores":[95,87,92],"address":{"city":"London","zip":"SW1A"}}
{
"currentState": "UNLOCKED",
"model": {
"$": {
"#.address": "OBJECT",
".name": "STRING",
".scores[*]": "INTEGER"
},
"$.address": {
".city": "STRING",
".zip": "STRING"
}
}
}

Array of arrays. One [*] per level, in the parent node. There is no detached node for the inner array.

// posted: {"matrix":[[1,2,3],[4,5,6]]}
{
"currentState": "UNLOCKED",
"model": { "$": { ".matrix[*][*]": "INTEGER" } }
}

Polymorphic scalar, after ingesting {"data":"hello"} and {"data":42}:

{
"currentState": "UNLOCKED",
"model": { "$": { ".data": "[INTEGER, STRING]" } }
}

Heterogeneous array. Element types merge into one set, and a null contributes no type:

// posted: {"row":[1,null,"three"]}
{
"currentState": "UNLOCKED",
"model": { "$": { ".row[*]": "[INTEGER, STRING]" } }
}

Kind union across a container and a scalar, after ingesting {"data":[{"nested":"primitive"}]} and {"data":[[123,321],[456,654]]}:

{
"currentState": "UNLOCKED",
"model": {
"$": {
"#.data": "OBJECT",
".data[*][*]": "INTEGER"
},
"$.data[*]": {
"#": "ARRAY_ELEMENT",
".nested": "STRING"
}
}
}

Two levels of object arrays:

// posted: {"orders":[{"lines":[{"sku":"AX-1","qty":2}]}]}
{
"currentState": "UNLOCKED",
"model": {
"$": { "#.orders": "OBJECT" },
"$.orders[*]": { "#": "ARRAY_ELEMENT", "#.lines": "OBJECT" },
"$.orders[*].lines[*]": {
"#": "ARRAY_ELEMENT",
".qty": "INTEGER",
".sku": "STRING"
}
}
}
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SIMPLE_VIEW Model Export Response",
"description": "Response from GET /model/export/SIMPLE_VIEW/{entityName}/{modelVersion}",
"type": "object",
"required": ["currentState", "model"],
"additionalProperties": false,
"properties": {
"currentState": {
"type": "string",
"enum": ["LOCKED", "UNLOCKED"],
"description": "Lifecycle state of the entity model."
},
"model": {
"type": "object",
"description": "Map of node paths to node descriptors. Always contains at least the root node '$'.",
"propertyNames": {
"pattern": "^\\$(\\.[A-Za-z0-9_-]+(\\[\\*\\])*)*$"
},
"additionalProperties": { "$ref": "#/$defs/nodeDescriptor" }
},
"uniqueKeys": {
"type": "array",
"description": "Composite unique keys declared on the model. Omitted when none are declared.",
"items": {
"type": "object",
"required": ["id", "fields"],
"properties": {
"id": { "type": "string" },
"fields": {
"type": "array",
"items": { "type": "string" },
"minItems": 1
}
}
}
}
},
"$defs": {
"dataType": {
"type": "string",
"enum": [
"INTEGER", "LONG", "BIG_INTEGER", "UNBOUND_INTEGER",
"DOUBLE", "BIG_DECIMAL", "UNBOUND_DECIMAL",
"STRING", "CHARACTER",
"LOCAL_DATE", "LOCAL_DATE_TIME", "LOCAL_TIME",
"ZONED_DATE_TIME", "YEAR", "YEAR_MONTH",
"UUID_TYPE", "TIME_UUID_TYPE",
"BYTE_ARRAY", "BOOLEAN", "NULL"
]
},
"typeDescriptor": {
"description": "One DataType name, or a bracketed set when the field declares several.",
"type": "string",
"examples": ["STRING", "INTEGER", "NULL", "[INTEGER, STRING]"]
},
"structuralDataType": {
"type": "string",
"description": "Value of a '#'-prefixed structural key.",
"enum": ["OBJECT", "ARRAY_ELEMENT"]
},
"nodeDescriptor": {
"type": "object",
"description": "Keys prefixed with '.' are data fields; keys prefixed with '#' are structural markers.",
"propertyNames": {
"pattern": "^((\\.[A-Za-z0-9_-]+(\\[\\*\\])*)|(#(\\.[A-Za-z0-9_-]+)?))$"
},
"additionalProperties": {
"anyOf": [
{ "$ref": "#/$defs/typeDescriptor" },
{ "$ref": "#/$defs/structuralDataType" }
]
}
}
}
}

The JSON_SCHEMA converter renders the same model as a JSON Schema document under the same envelope. Objects become {"type": "object", "properties": …}, arrays become {"type": "array", "items": …}, and leaves map onto JSON Schema primitives:

DataType Rendered as
INTEGER, LONG, BIG_INTEGER, UNBOUND_INTEGER {"type": "integer"}
DOUBLE, BIG_DECIMAL, UNBOUND_DECIMAL {"type": "number"}
STRING, CHARACTER {"type": "string"}
BOOLEAN {"type": "boolean"}
NULL {"type": "null"}
LOCAL_DATE {"type": "string", "format": "date"}
LOCAL_DATE_TIME, ZONED_DATE_TIME {"type": "string", "format": "date-time"}
LOCAL_TIME {"type": "string", "format": "time"}
YEAR {"type": "string", "format": "year"}
YEAR_MONTH {"type": "string", "format": "year-month"}
UUID_TYPE, TIME_UUID_TYPE {"type": "string", "format": "uuid"}
BYTE_ARRAY {"type": "string", "format": "byte"}

A field that declares more than one branch renders as anyOf, never oneOf. Two DataType values can render the same shape, because INTEGER and LONG both give {"type": "integer"}, so a union may carry identical branches.

// posted: {"data":"hello"} then {"data":42}
{
"currentState": "UNLOCKED",
"model": {
"type": "object",
"properties": {
"data": { "anyOf": [{ "type": "integer" }, { "type": "string" }] }
}
}
}

The same rule covers a kind union. A field seen as both a scalar and an array renders as an anyOf over the array branch and the scalar branch:

// posted: {"poly":"a"} then {"poly":["b","c"]}
{
"currentState": "UNLOCKED",
"model": {
"type": "object",
"properties": {
"poly": {
"anyOf": [
{ "type": "array", "items": { "type": "string" } },
{ "type": "string" }
]
}
}
}
}
Status Condition Error code
404 Model not found MODEL_NOT_FOUND
400 Unknown converter value BAD_REQUEST

404 example (captured):

{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"detail": "MODEL_NOT_FOUND: cannot find model entityName=nobel-prize, version=2",
"instance": "/model/export/SIMPLE_VIEW/nobel-prize/2",
"properties": {
"entityName": "nobel-prize",
"entityVersion": 2,
"errorCode": "MODEL_NOT_FOUND"
}
}
  1. Output is deterministic. Node paths are sorted lexicographically, and keys within a node are sorted, so two exports of the same model are byte-identical.

  2. Models evolve by merging. When you ingest entities under a model whose change level permits it, new fields appear, the declared types of a leaf widen (INTEGER → [INTEGER, STRING]), and a field can declare a new kind. The export does not report the length of an array.

  3. Every declared branch is exported. A field that declares more than one kind names each kind. The export describes what the model enforces.

  4. An export is not an import format. SAMPLE_DATA is the only converter that the import endpoint accepts. A POST of an export to /model/import/JSON/SIMPLE_VIEW/… returns 400 BAD_REQUEST: unsupported import converter. To establish a model, post representative sample documents.