DataSync schemas and validation
Schemas in DataSync are declared on classes through property definitions. A property definition marks one payload field for validation, filtering, and projection membership at once. A class's time-to-live (TTL) setting expires stale objects automatically. Adding a new class version lets you evolve your types over time while existing instances keep the validation, filtering, and access behavior of the version they were created against.
Classes and versions
A class can be a bare name with payloads stored as-is, or a class with declared properties, searchable fields, and field-level access through projections. Every entity class has a TTL, defaulting to 2,678,400 seconds (31 days).
Once you've declared a class with Create a new entity class or Create a new relationship class, or in the Admin Portal, you can create instances from anywhere. Your team manages types centrally, while clients create and update instances at runtime.
A class is a name plus an integer version. A class name can be up to 128 characters, must start with a letter, and can otherwise contain only letters, digits, hyphens, and underscores. Multiple versions of the same class can coexist, and version numbers don't have to be sequential. Listing or filtering instances without naming a version matches every version of the class.
Each entity or relationship instance records the version it was created against, but that version isn't frozen. You can move an instance to a different version of the same class by sending a new entityClassVersion on a replace, or a JSON Patch to /entityClassVersion. What can't change is entityClass (or relationshipClass) itself, so a user entity can't become a channel entity.
Versions let you evolve a type without breaking existing data. You could define a class at version 1, then add a field in version 2. Existing version 1 instances keep working unchanged. Both versions stay live, and each instance records which one it currently uses.
Relationship class settings
A relationship class declares cardinality (one-to-one, one-to-many, or many-to-many), enforced when a relationship is created. See Data model.
A relationship class can also restrict which entity class is allowed on each side, through entityAClass and entityBClass. Leave a side unset and any entity class can be used there. A side restriction accepts subclasses of the named class as well as exact matches.
Each side also takes an optional class level, entityAClassLevel and entityBClassLevel, which disambiguates a keyset-level class from a Global class of the same name. Left unset, a keyset-level class takes precedence over a Global one.
Property definitions
A class can declare property definitions. A property definition marks one payload field for filtering, projection membership, and write-time validation all at once. A payload with a missing non-nullable property, a value of the wrong type, or an array or object where a scalar is expected is rejected with a 400.
| Attribute | Required | Purpose |
|---|---|---|
name | Yes | The property's name, used in filter and sort expressions |
path | Yes | A JSON Pointer to the field, starting at the whole object, for example /payload/price |
valueKind | Yes | string, number, boolean, date, or datetime |
filtering | No | none, simple, or full. Defaults to none |
isNullable | No | Whether the field may be missing or null. Defaults to true |
projections | No | Which named projections include this field. Defaults to __default__ |
Four property names are reserved
id, createdAt, and updatedAt are reserved and can't be used as property names at all. status may only be declared as {"name": "status", "path": "/status"} to put the native field in a projection. All other uses of these four names fail class creation with a 400 (DS-0906).
Property paths start at the object, not the payload
A property path addresses the whole object, where payload sits alongside the system fields. The price field on a product entity is /payload/price, not /price. A nullable property at an incorrect path is silently skipped. A non-nullable property at an incorrect path makes every write for that class fail with DS-0650 NON_NULLABLE_PROPERTY.
The filtering mode controls how DataSync stores that field for querying:
| Mode | How the field is stored | What you can do with it |
|---|---|---|
none | Not extracted for querying | Validated only; not filterable or sortable |
simple | Extracted into the strongly consistent store | filter_fast filtering and sorting |
full | Extracted into both stores | Both filter_fast and filter, plus sorting |
full is a superset of simple. A full property works with filter_fast as well, so choosing full never costs you strongly consistent filtering.
Declare properties before you need them
Property definitions apply forward-only. Data written before a property was declared on the class is not retroactively indexed, so it won't appear in filters on that field until it is rewritten.
Property definitions also control projection membership, which is how field-level read and write access is scoped. See Projections.
Updating a class version replaces its entire property-definition set, rather than merging into it. Partial class updates aren't implemented. Send every property you want the version to keep. Omitting properties deletes all of them.
Class inheritance
Class inheritance lets you build new classes on top of existing ones.
Entity class inheritance
Entity classes support single inheritance, upward across class levels only. Because the classes you create are keyset-level, you can extend the Global User and Channel classes but not your own other classes. Extending a same-level class is rejected with a 400.
Inheritance merges parent properties into the subclass automatically. You only declare the properties you're adding. A subclass can't drop an inherited property. If you redeclare a property with the same name as one on the parent, the path, valueKind, and isNullable must match the parent exactly. The filtering mode and projections are free to differ.
Querying a parent class also returns instances of its subclasses.
Relationship class inheritance
Relationship classes don't support inheritance. The built-in Membership class has no extension path. You can still store arbitrary fields in a membership's payload, but they aren't filterable.
Data expiry (TTL)
Entity classes carry a config.ttlSec setting. The default is 2,678,400 seconds (31 days). The minimum is 0 and the maximum is 315,569,260 seconds (approximately 10 years). There is no never-expire option.
A class created with extends and no config of its own inherits its parent's TTL. A class extending the Global User class without its own config gets User's 2,592,000 seconds (30 days) TTL, not 31 days.
The expiry mechanics that matter when you design a class:
expiresAtis computed once, at creation, as the creation time plus the class TTL, rounded up to the start of the next whole UTC day.- Updating an instance does not extend or refresh its
expiresAt. - There is no per-instance override of the class TTL.
- Once
expiresAtpasses, the object stops being readable. - A relationship expires at the earlier of its two linked entities' expiry times, computed when the relationship is created.
Plan for expiry
For long-lived data such as user profiles, set a long TTL. expiresAt rounds up to the next whole UTC day, so a short TTL gives you day-granularity cleanup, not minute-granularity.
Managing classes
Classes, property definitions, and event rules are managed with the Admin API or in the Admin Portal. See the Admin API reference for the endpoint list.
Some parts of a class are fixed when you create it:
- Fixed at creation:
cardinality,entityAClass,entityBClass, and theextendsreference. - Editable later: property definitions, the class description, and TTL. Editing a live class version affects every instance pointing at it, so evolving a type usually means adding a new version.
A class version can be deleted. Deleting a version that another class extends is rejected with a 409. Global class versions can't be deleted.