Skip to main content

Entity metamodel API

The curation API works with entities: a table, a dashboard, a tag. This API works one level up, with the shapes those entities take — the entity types, and the attributes each type carries.

An entity type is a class; an entity is an instance of it. An attribute definition declares that instances of a type may carry a value of a given kind, under a given name. Nothing here writes values; that is the curation API.

All endpoints require authentication and return the standard DataDios response envelope:

{
"STATUS": "SUCCESS",
"MESSAGE": "",
"DATA": {}
}

A refused request has STATUS: "FAIL" and a machine-readable code in DATA.reason. Branch on DATA.reason, never on the text in MESSAGE.

Endpoints

Method and pathPurposeSuccess status
POST /entities/typesCreate an entity type201 when created; 200 when the name already exists
GET /entities/typesList entity types200
POST /entities/types/{guid}/attributesDeclare an attribute on an entity type201
GET /entities/types/{guid}/attributesList the attributes that apply to a type200

Create an entity type

{
"name": "Data Product"
}

Creating a type is create-or-get, not create-or-update. Names are compared after trimming spaces and ignoring letter case. If the name already resolves to a type, the API returns that type with "created": false and writes nothing.

Two consequences follow from the identity rule, and both are easier to know than to discover:

  • Creating "Data Product" twice converges on one type. There is no way to end up with two.
  • A type's identity is derived from its name, so a type that is deleted and later recreated under the same name comes back with the same guid. Any curation that outlived the delete points at it again.

The response carries origin, which separates the types DataDios ships from the ones you create:

{
"entity_type": { "guid": "...", "name": "Data Product", "origin": "user_defined" },
"created": true
}

List entity types

GET /entities/types returns both the seeded system types and your own, told apart by origin. You need that distinction: only user-created types can be renamed or deleted.

The listing is paged with a cursor. Pass cursor and page_size as query parameters; the response carries next_cursor, which is null on the last page.

{
"entity_types": [{ "guid": "...", "name": "Data Product", "origin": "user_defined" }],
"next_cursor": null
}

Declare an attribute

{
"name": "sla_tier",
"value_type": "string",
"cardinality": "multi",
"allowed_values": ["gold", "silver", "bronze"],
"required": false,
"default_value": "bronze",
"filterable": false
}
FieldRequiredMeaning
nameyesAttribute name, unique within the owning type
value_typeyesOne of string, number, boolean, date, json, entity_ref
cardinalityyessingle for one value, multi for a list
allowed_valuesnoEnumerated values, for scalar types only
allowed_ref_typefor refsGuid of the type an entity_ref attribute points at
requirednoWhether instances must carry a value
default_valuenoApplied when an instance is created without one; an entity guid for references
filterablenoWhether the attribute can be filtered on; entity_ref only

Every malformed combination is refused with its own reason code rather than being quietly ignored. The rules that are easiest to get wrong:

  • allowed_values is for scalars only. A reference attribute is constrained by allowed_ref_type instead. Sending both is refused.
  • An entity_ref attribute must declare allowed_ref_type. Without it the attribute can never accept a value by name — only by guid — for the rest of its life, so this is refused at declaration rather than discovered later.
  • filterable applies to entity_ref only. Filtering is built on reference values, so a filterable string attribute would look filter-ready and match nothing, permanently.

Limits

LimitValueRefusal reason
Attributes declared on one type200too_many_attribute_defs
Entries in allowed_values1,000too_many_allowed_values
Request body, before it is parsed8 MBrequest_too_large (413)

The first two are not about the size of a single request. A type's attribute definitions are read on every curation write against that type, so an unbounded set is a cost paid by every later write rather than by the call that declared them.

Attribute names cannot be changed

An attribute's declaration is fixed once created. There is no rename and no edit: changing any field means withdrawing the attribute and declaring a new one. filterable is the field callers most often want to flip, and flipping it would rewrite the stored filter data of every instance of the owning type — a background job, not a field edit.

To withdraw one, use DELETE /entities/attributes/{type_guid}/{name} from the curation API.

List a type's attributes

GET /entities/types/{guid}/attributes returns every attribute that applies to the type: the ones declared on it, plus the four platform attributes every type carries — domain, env, tags, and classification.

A brand-new type with no attributes of its own still lists those four. They are owned by the framework rather than by any one type, so they come back with "owner_type": null. That null is deliberate and is not a missing value: naming an owning type would claim a per-type attribute where the platform has a schema-wide one.

Withdrawn attributes are absent from this listing.

{
"attributes": [
{
"guid": "...",
"name": "tags",
"owner_type": null,
"value_type": "entity_ref",
"cardinality": "multi",
"allowed_values": null,
"allowed_ref_type": "...",
"required": false,
"default_value": null,
"filterable": true,
"origin": "system"
}
]
}

This listing is not paged. It returns the type's full applicable attribute set, which the 200-per-type limit above bounds.

Refusal reasons

ReasonStatusWhen
attribute_def_already_exists409An attribute of that name is already declared on the type
attribute_def_withdrawn409An attribute of that name was withdrawn and its data is still being removed
too_many_attribute_defs400The type is at the 200-attribute limit
base_type_not_owner400An attribute was declared against the platform's own attribute owner
unknown_entity_type404The type guid does not resolve
unknown_value_type400value_type is outside the six
invalid_cardinality400cardinality is neither single nor multi
allowed_values_not_supported400allowed_values was sent on an entity_ref attribute
allowed_ref_type_not_supported400allowed_ref_type was sent on a non-reference attribute
allowed_ref_type_required400An entity_ref attribute omitted allowed_ref_type
filterable_not_supported400filterable was set on a non-reference attribute
referent_not_found404A reference default_value does not resolve
ref_type_mismatch400An entity_ref default_value names an entity of the wrong type
empty_value400name normalizes to nothing once spaces are trimmed
invalid_page_size400page_size on the type listing is outside the permitted range
invalid_cursor400cursor on the type listing is not one this API issued
request_too_large413The body exceeds 8 MB

Both 409s are temporary in different ways

attribute_def_already_exists means the attribute is live. Declaring it again is refused rather than treated as a no-op, because a declaration carries a shape: answering SUCCESS would report a definition you may not have asked for. Withdraw it first if you need a different shape.

attribute_def_withdrawn means the attribute was withdrawn but its stored values have not finished being removed. That refusal clears on its own — a daily job deletes the withdrawn definition once its data is gone, after which the name can be declared afresh. It is a window, not a permanent block.