---
source_url: https://www.pubnub.com/docs/data-storage/structured-data/schemas
title: DataSync schemas and validation
updated_at: 2026-09-30T07:20:08.000Z
---

# DataSync schemas and validation

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

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](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) or [Create a new relationship class](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md), or in the [Admin Portal](https://admin.pubnub.com), 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](https://www.pubnub.com/docs/data-storage/structured-data/data-model.md#relationships).

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__` |

:::warning 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`).
:::

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

:::warning 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](https://www.pubnub.com/docs/data-storage/structured-data/projections.md).

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:

* `expiresAt` is 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 `expiresAt` passes, the object stops being readable.
* A relationship expires at the earlier of its two linked entities' expiry times, computed when the relationship is created.

:::note 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](https://admin.pubnub.com). See the [Admin API reference](https://www.pubnub.com/docs/admin-api.md) for the endpoint list.

Some parts of a class are fixed when you create it:

* Fixed at creation: `cardinality`, `entityAClass`, `entityBClass`, and the `extends` reference.
* 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.

Last updated at: 2026-09-30T07:20:08.000Z
