---
source_url: https://www.pubnub.com/docs/data-storage/structured-data/define-entity-class
title: Define an entity class
updated_at: 2026-09-30T07:20:08.000Z
---

# Define an entity class

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

This guide shows you how to create an entity class with property definitions on your keyset using the Admin API. Property definitions control write-time validation, filtering, and field-level access for every entity you create against that class.

## Before you start

* DataSync must be enabled on your keyset. Refer to [Enable DataSync](https://www.pubnub.com/docs/data-storage/structured-data/enable-structured-data.md).
* You need an Admin API key. [Create a Service Integration](https://www.pubnub.com/docs/admin-api.md) with a permission row for the **DataSync** resource, at **Keyset** level for your keyset, with **Read & write** access. Copy the API key when you create it, because the Admin Portal shows it only once.
* Keep the API key on your server. It manages class definitions only. Your clients create and read entities with Access Manager tokens instead, as described in [DataSync access control](https://www.pubnub.com/docs/data-storage/structured-data/access-control.md).

## Create an entity class

Call [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md). The class name and version go in the path, and the class definition goes in the `data` object of the body.

```bash
curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/Product/versions/1' \
  -H 'Authorization: {apiKey}' \
  -H 'PubNub-Version: 2026-09-17' \
  -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
  -d '{
    "data": {
      "config": {
        "ttlSec": 2678400
      },
      "properties": [
        {
          "name": "name",
          "path": "/payload/name",
          "valueKind": "string",
          "filtering": "full",
          "isNullable": true,
          "projections": [{"name": "__default__"}, {"name": "public"}]
        },
        {
          "name": "price",
          "path": "/payload/price",
          "valueKind": "number",
          "filtering": "simple",
          "isNullable": false,
          "projections": [{"name": "__default__"}, {"name": "public"}]
        },
        {
          "name": "stock",
          "path": "/payload/stock",
          "valueKind": "number",
          "filtering": "simple",
          "isNullable": true,
          "projections": [{"name": "__default__"}]
        }
      ]
    }
  }'
```

| Part | Value |
| --- | --- |
| `{subKey}` | The subscribe key of the keyset. |
| `Product`, `1` | The class name and the class version, in the path. |
| `Authorization` | The Service Integration API key. |
| `PubNub-Version` | The Admin API version. A request without this header is rejected with a `428`. |
| `Content-Type` | Must be `application/vnd.pubnub.objects.entity-class+json;version=1`. `application/json` is rejected with a `415`. |
| `config` | Required for a class that doesn't extend another class. Send `{}` to use the default TTL. |

A successful request returns `201 Created` with the stored class:

```json
{
  "data": {
    "level": "SubKey",
    "createdAt": "2026-09-23T21:35:15.747618137Z",
    "updatedAt": "2026-09-23T21:35:15.747618137Z",
    "eTag": "3w5e111sv9tng",
    "version": 1,
    "name": "Product",
    "config": {"ttlSec": 2678400},
    "properties": [
      {
        "name": "name",
        "path": "/payload/name",
        "valueKind": "string",
        "filtering": "full",
        "isNullable": true,
        "projections": [{"name": "__default__"}, {"name": "public"}]
      },
      {
        "name": "price",
        "path": "/payload/price",
        "valueKind": "number",
        "filtering": "simple",
        "isNullable": false,
        "projections": [{"name": "__default__"}, {"name": "public"}]
      },
      {
        "name": "stock",
        "path": "/payload/stock",
        "valueKind": "number",
        "filtering": "simple",
        "isNullable": true,
        "projections": [{"name": "__default__"}]
      }
    ]
  }
}
```

`level` is `SubKey` for every class you define on a keyset. The built-in `User` and `Channel` classes have `level` set to `Global`.

A `price` property declared as `filtering: simple` validates every write and makes `price` filterable with `filter_fast`. A `name` property declared as `filtering: full` is available for both `filter_fast` and eventually consistent `filter` queries.

The `projections` list on each property controls who sees it. A client reading through the `public` projection gets `name` and `price` but not `stock`. Refer to [Projections](https://www.pubnub.com/docs/data-storage/structured-data/projections.md).

If the request fails, the response lists the reasons in `errors`. The most common ones:

| Status | Error code | Cause |
| --- | --- | --- |
| `400` | `DS-0006` | `config` is missing on a class that doesn't extend another class. |
| `400` | `DS-0004` | A body field failed validation, for example a body not wrapped in `data`. The `path` field names the field. |
| `409` | `DS-0301` | This class name and version already exist on the keyset. |
| `415` | `DS-0007` | The `Content-Type` isn't supported. |

For every code, refer to [DataSync error codes](https://www.pubnub.com/docs/data-storage/structured-data/error-codes.md).

## Choosing a filtering mode

| Mode | Use when |
| --- | --- |
| `none` | Validation only; you'll never filter on this field |
| `simple` | You need strongly consistent filtering (`filter_fast`) |
| `full` | You need both filtering tiers, or eventually consistent search |

If you plan to combine a property with `filter` (eventually consistent) in a sort expression, declare it as `full`. A `simple` property sorts correctly under `filter_fast` but is rejected when combined with `filter`.

## Set a class TTL

Every entity class has a `config.ttlSec` setting. Send `config` without `ttlSec`, as `{}`, and DataSync uses the default of 2,678,400 seconds (31 days). To keep entities longer, for example long-lived user profiles, set a higher value. The maximum is 315,569,260 seconds (approximately 10 years).

```bash
curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/UserProfile/versions/1' \
  -H 'Authorization: {apiKey}' \
  -H 'PubNub-Version: 2026-09-17' \
  -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
  -d '{
    "data": {
      "config": {
        "ttlSec": 31536000
      }
    }
  }'
```

## Extend a Global class

To add fields to users or channels, define a class that extends the Global `User` or `Channel` class, rather than trying to modify the Global class itself. The `extends` reference takes the parent's `name` and `version`. PubNub seeds `User` and `Channel` at version 1.

```bash
curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/entity-classes/MarketplaceUser/versions/1' \
  -H 'Authorization: {apiKey}' \
  -H 'PubNub-Version: 2026-09-17' \
  -H 'Content-Type: application/vnd.pubnub.objects.entity-class+json;version=1' \
  -d '{
    "data": {
      "extends": {
        "name": "User",
        "version": 1
      },
      "properties": [
        {
          "name": "display_name",
          "path": "/payload/display_name",
          "valueKind": "string",
          "filtering": "full",
          "isNullable": true,
          "projections": [{"name": "__default__"}]
        },
        {
          "name": "email",
          "path": "/payload/email",
          "valueKind": "string",
          "filtering": "none",
          "isNullable": true,
          "projections": [{"name": "admin"}]
        }
      ]
    }
  }'
```

The subclass inherits the `name` and `type` properties from the Global `User` class automatically, so you only declare the fields you're adding. Without its own `config`, it also inherits `User`'s TTL of 2,592,000 seconds (30 days).

`email` belongs only to the `admin` projection. A client that reads a `MarketplaceUser` without the `admin` projection doesn't get `email`.

## Define a relationship class

To link two entities with your own relationship type, define a relationship class. This `Wishlist` class links a `MarketplaceUser` to a `Product`. Relationship classes use their own endpoint and content type.

```bash
curl -X POST 'https://admin-api.pubnub.com/v2/datasync/subkeys/{subKey}/relationship-classes/Wishlist/versions/1' \
  -H 'Authorization: {apiKey}' \
  -H 'PubNub-Version: 2026-09-17' \
  -H 'Content-Type: application/vnd.pubnub.objects.relationship-class+json;version=1' \
  -d '{
    "data": {
      "cardinality": "many-to-many",
      "entityAClass": "MarketplaceUser",
      "entityBClass": "Product"
    }
  }'
```

`cardinality` is `one-to-one`, `one-to-many`, or `many-to-many`. `entityAClass` and `entityBClass` restrict which entity class each side accepts. Refer to [Create a new relationship class](https://www.pubnub.com/docs/admin-api/create-a-new-relationship-class.md).

## Add a new class version

To evolve a class without breaking existing instances, add a new version. Existing instances keep the validation and filtering behavior of the version they were created against.

Call [Create a new entity class](https://www.pubnub.com/docs/admin-api/create-a-new-entity-class.md) with the same class name and a new integer version in the path, for example `/entity-classes/Product/versions/2`.

:::warning Updating a version replaces all property definitions
Updating a class version replaces its entire property-definition set. Partial class updates aren't implemented. Send every property you want the version to keep, including inherited ones you want to redeclare.
:::

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