1. Defining Schemas
Define schemas to represent the JSON returned by an endpoint. Compose these
to represent the data expected.
Object
- Entity - represents a single unique object (denormalized)
- EntityMixin - turn any pre-existing class into an Entity
- new Union(Entity) - polymorphic objects (A | B)
{[key:string]: Schema} - immutable objects
- new Invalidate(Entity|Union) - to delete an Entity
- new Lazy(() => Schema) - break circular imports / defer deep recursive denormalization
List
- new Collection([Schema]) - mutable/growable lists
[Schema] - immutable lists
- new All(Entity|Union) - list all Entities of a kind
Map
new Collection(Values(Schema)) - mutable/growable maps
- new Values(Schema) - immutable maps
Lens-dependent entity fields
- new Scalar({ lens, key, entity? }) - fields that vary by runtime lens (portfolio, currency, locale) without entity mutation
Derived / selector pattern
new Query(Queryable) - memoized programmatic selectors
const queryRemainingTodos = new Query(
TodoResource.getList.schema,
entries => entries.filter(todo => !todo.completed).length,
);
const groupTodoByUser = new Query(
TodoResource.getList.schema,
todos => Object.groupBy(todos, todo => todo.userId),
);
Define Query transformations with the data model (e.g. src/resources/) — not inside custom hooks
wrapping useSuspense/useQuery, which hides data dependencies and couples data logic to view code.
2. Entity best practices
- Every
Entity subclass defines defaults for all non-optional serialised fields.
- Override
pk() only when the primary key ≠ id.
pk() return type is number | string | undefined
- Override
Entity.process(value, parent, key, args) to insert fields based on args/url
static schema (optional) for nested schemas or deserialization functions
- When designing APIs, prefer nesting entities
3. Entity lifecycle methods
- Normalize (JSON response → cache): operates on POJOs; output is JSON-serializable plain data stored in the normalized cache. Order:
process() → pk() → validate() → visit nested schemas (recurse into schema fields) → if existing: mergeWithStore() which calls shouldUpdate() and maybe shouldReorder() + merge(); metadata via mergeMetaWithStore().
- Denormalize (cache → component): creates Entity class instances via
fromJS(), restoring prototype chain so getters, methods, and schema processing work. Order: createIfValid() → validate() → fromJS() → unvisit nested schemas (recurse into schema fields).
4. Union Types (Polymorphic Schemas)
To define polymorphic resources (e.g., events), use Union and a discriminator field.
import { Union } from '@data-client/rest'; // also available from @data-client/endpoint
export abstract class Event extends Entity {
type: EventType = 'Issue'; // discriminator field is shared
/* ... */
}
export class PullRequestEvent extends Event { /* ... */ }
export class IssuesEvent extends Event { /* ... */ }
export const EventResource = resource({
path: '/users/:login/events/public/:id',
schema: new Union(
{
PullRequestEvent,
IssuesEvent,
// ...other event types...
},
'type', // discriminator field
),
});
5. Collections (Mutable Lists & Maps)
Collections wrap Array or Values schemas to enable mutations (add/remove/move).
pk routing
pk() uses nestKey(parent, key) when nested in an Entity and available; otherwise it uses argsKey(...args), then serializes the result. Without options, it defaults to argsKey: params => ({ ...params }), using all endpoint args as the collection key.
argsKey — derive pk from endpoint arguments (default)
nestKey — derive pk from parent entity for nested shared-state collections
Define both on the same Collection to reuse one definition top-level and nested. When argsKey(args) and nestKey(parent) produce the same object shape, the top-level fetch and the nested read resolve to the same (referentially equal) array/map — push/unshift/assign/move/remove on either updates both:
const userTodos = new Collection([Todo], {
argsKey: ({ userId }: { userId?: string }) => ({ userId }),
nestKey: (parent: User) => ({ userId: parent.id }),
});
nonFilterArgumentKeys
Default createCollectionFilter uses nonFilterArgumentKeys (default: keys starting with 'order') to exclude non-filter args when matching collections. This affects which existing collections receive new items from push/unshift/assign/move.
Override as function, RegExp, or string[]:
new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })
Extenders
All usable with ctrl.set() (local-only) or via RestEndpoint extenders (network).
| Method |
Type |
Description |
push |
Array |
Entity |
unshift |
Array |
Entity |
assign |
Values |
Merge entries into map |
remove |
Both |
Remove items by value from matching collections |
move |
Both |
Remove from collections matching existing state, add to collections matching new state |
addWith(merge, filter?) |
Both |
Custom creation schema (used internally by push/unshift/assign) |
moveWith(merge) |
Both |
Custom move schema (control insertion order, e.g., unshift merge for prepending) |
6. Supplementary Endpoints (enrich existing entities)
When an endpoint returns partial or differently-shaped data for an entity already in cache
(e.g., a metadata endpoint, a stats endpoint, a lazy-load expansion endpoint),
use the same Entity as the schema — don't create a wrapper entity.
See partial-entities for patterns and examples.
7. Best Practices & Notes
- Always set up
schema on every resource/entity/collection for normalization
- Normalize deeply nested or relational data by defining proper schemas
- Use
Entity.schema for client-side joins
- Use
Denormalize<> type from rest/endpoint/graphql instead of InstanceType<>. This will handle all schemas like Unions, not just Entity.
8. Common Mistakes to Avoid
- The normalized cache stores plain JSON-serializable objects (POJOs), not class instances.
- Don't forget to use
fromJS() or assign default properties for class fields — bare TS field types emit no runtime defaults, so schema inference breaks
- Manually merging or 'enriching' data; instead use
Entity.schema for client-side joins
References
For detailed API documentation, see the references directory:
- Entity - Normalized data class
- EntityMixin - Turn any class into an Entity
- Collection - Mutable/growable lists
- Union - Polymorphic schemas
- Query - Programmatic selectors
- Invalidate - Delete entities
- Lazy - Deferred / circular schemas
- Scalar - Lens-dependent entity fields
- Values - Map schemas
- All - List all entities of a kind
- Array - Immutable list schema
- Object - Object schema
- schema - Schema overview
- relational-data - Relational data guide
- computed-properties - Computed properties guide
- partial-entities - Partial entities guide
- side-effects - Side effects guide
- sorting-client-side - Client-side sorting guide
1---2name: data-client-schema3description: Model data with @data-client schemas (Entity, EntityMixin, Collection, Union, Query, Values, All, Invalidate, Lazy, Scalar) for atomic, consistent, referentially-equal async data via normalization, identity-based caching, and a single source of truth. Use when defining or editing pk, static schema, resource()/RestEndpoint schema, mutable lists/maps (push/unshift/assign/remove/move), polymorphic/discriminated types, memoized selectors / derived data, partial/supplementary entities, relational/nested/joined data, optimistic updates, or cache invalidation across @data-client/rest, /endpoint, /graphql, or /normalizr. Apply proactively when discussing data models, remote data shape, caching, normalization, identity, joins, polymorphism, mutable collections, or store consistency.4license: Apache 2.05---67## 1. Defining Schemas89Define [schemas](references/schema.md) to represent the JSON returned by an endpoint. Compose these10to represent the data expected.1112### Object1314- [Entity](references/Entity.md) - represents a single unique object (denormalized)15- [EntityMixin](references/EntityMixin.md) - turn any pre-existing class into an Entity16- [new Union(Entity)](references/Union.md) - polymorphic objects (A | B)17- [`{[key:string]: Schema}`](references/Object.md) - immutable objects18- [new Invalidate(Entity|Union)](references/Invalidate.md) - to delete an Entity19- [new Lazy(() => Schema)](references/Lazy.md) - break circular imports / defer deep recursive denormalization2021### List2223- [new Collection([Schema])](references/Collection.md) - mutable/growable lists24- [`[Schema]`](references/Array.md) - immutable lists25- [new All(Entity|Union)](references/All.md) - list all Entities of a kind2627### Map2829- `new Collection(Values(Schema))` - mutable/growable maps30- [new Values(Schema)](references/Values.md) - immutable maps3132### Lens-dependent entity fields3334- [new Scalar({ lens, key, entity? })](references/Scalar.md) - fields that vary by runtime lens (portfolio, currency, locale) without entity mutation3536### Derived / selector pattern3738- [new Query(Queryable)](references/Query.md) - memoized programmatic selectors39 ```ts40 const queryRemainingTodos = new Query(41 TodoResource.getList.schema,42 entries => entries.filter(todo => !todo.completed).length,43 );44 ```4546 ```ts47 const groupTodoByUser = new Query(48 TodoResource.getList.schema,49 todos => Object.groupBy(todos, todo => todo.userId),50 );51 ```5253 Define `Query` transformations with the data model (e.g. `src/resources/`) — not inside custom hooks54 wrapping useSuspense/useQuery, which hides data dependencies and couples data logic to view code.5556---5758## 2. Entity best practices5960- Every `Entity` subclass **defines defaults** for _all_ non-optional serialised fields.61- Override `pk()` only when the primary key ≠ `id`.62- `pk()` return type is `number | string | undefined`63- Override `Entity.process(value, parent, key, args)` to insert fields based on args/url64- `static schema` (optional) for nested schemas or deserialization functions65 - When designing APIs, prefer nesting entities6667---6869## 3. Entity lifecycle methods7071- **Normalize** (JSON response → cache): operates on POJOs; output is JSON-serializable plain data stored in the normalized cache. Order: `process()` → `pk()` → [validate()](references/validation.md) → **visit nested schemas** (recurse into `schema` fields) → if existing: `mergeWithStore()` which calls `shouldUpdate()` and maybe `shouldReorder()` + `merge()`; metadata via `mergeMetaWithStore()`.72- **Denormalize** (cache → component): creates Entity **class instances** via `fromJS()`, restoring prototype chain so getters, methods, and `schema` processing work. Order: `createIfValid()` → [validate()](references/validation.md) → `fromJS()` → **unvisit nested schemas** (recurse into `schema` fields).7374---7576## 4. **Union Types (Polymorphic Schemas)**7778To define polymorphic resources (e.g., events), use [Union](references/Union.md) and a discriminator field.7980```typescript81import { Union } from '@data-client/rest'; // also available from @data-client/endpoint8283export abstract class Event extends Entity {84 type: EventType = 'Issue'; // discriminator field is shared85 /* ... */86}87export class PullRequestEvent extends Event { /* ... */ }88export class IssuesEvent extends Event { /* ... */ }8990export const EventResource = resource({91 path: '/users/:login/events/public/:id',92 schema: new Union(93 {94 PullRequestEvent,95 IssuesEvent,96 // ...other event types...97 },98 'type', // discriminator field99 ),100});101```102103---104105## 5. Collections (Mutable Lists & Maps)106107[Collections](references/Collection.md) wrap `Array` or `Values` schemas to enable mutations (add/remove/move).108109### pk routing110111`pk()` uses `nestKey(parent, key)` when nested in an Entity and available; otherwise it uses `argsKey(...args)`, then serializes the result. Without options, it defaults to `argsKey: params => ({ ...params })`, using all endpoint args as the collection key.112113- `argsKey` — derive pk from endpoint arguments (default)114- `nestKey` — derive pk from parent entity for nested shared-state collections115116Define **both** on the same `Collection` to reuse one definition top-level and nested. When `argsKey(args)` and `nestKey(parent)` produce the same object shape, the top-level fetch and the nested read resolve to the **same (referentially equal) array/map** — push/unshift/assign/move/remove on either updates both:117118```ts119const userTodos = new Collection([Todo], {120 argsKey: ({ userId }: { userId?: string }) => ({ userId }),121 nestKey: (parent: User) => ({ userId: parent.id }),122});123```124125### nonFilterArgumentKeys126127Default `createCollectionFilter` uses `nonFilterArgumentKeys` (default: keys starting with `'order'`) to exclude non-filter args when matching collections. This affects which existing collections receive new items from `push`/`unshift`/`assign`/`move`.128129Override as function, RegExp, or `string[]`:130```ts131new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })132```133134### Extenders135136All usable with `ctrl.set()` (local-only) or via [RestEndpoint extenders](https://dataclient.io/rest/api/RestEndpoint) (network).137138| Method | Type | Description |139|--------|------|-------------|140| `push` | Array | Entity | Append items to end |141| `unshift` | Array | Entity | Prepend items to start |142| `assign` | Values | Merge entries into map |143| `remove` | Both | Remove items by value from matching collections |144| `move` | Both | Remove from collections matching existing state, add to collections matching new state |145| `addWith(merge, filter?)` | Both | Custom creation schema (used internally by push/unshift/assign) |146| `moveWith(merge)` | Both | Custom move schema (control insertion order, e.g., `unshift` merge for prepending) |147148---149150## 6. Supplementary Endpoints (enrich existing entities)151152When an endpoint returns partial or differently-shaped data for an entity already in cache153(e.g., a metadata endpoint, a stats endpoint, a lazy-load expansion endpoint),154use the **same Entity** as the schema — don't create a wrapper entity.155156See [partial-entities](references/partial-entities.md) for patterns and examples.157158---159160## 7. Best Practices & Notes161162- Always set up `schema` on every resource/entity/collection for normalization163- Normalize deeply nested or relational data by defining proper schemas164- Use `Entity.schema` for client-side joins165- Use `Denormalize<>` type from rest/endpoint/graphql instead of InstanceType<>. This will handle all schemas like Unions, not just Entity.166167## 8. Common Mistakes to Avoid168169- The normalized cache stores **plain JSON-serializable objects** (POJOs), not class instances.170- Don't forget to use `fromJS()` or assign default properties for class fields — bare TS field types emit no runtime defaults, so schema inference breaks171- Manually merging or 'enriching' data; instead use `Entity.schema` for client-side joins172173# References174175For detailed API documentation, see the [references](references/) directory:176177- [Entity](references/Entity.md) - Normalized data class178- [EntityMixin](references/EntityMixin.md) - Turn any class into an Entity179- [Collection](references/Collection.md) - Mutable/growable lists180- [Union](references/Union.md) - Polymorphic schemas181- [Query](references/Query.md) - Programmatic selectors182- [Invalidate](references/Invalidate.md) - Delete entities183- [Lazy](references/Lazy.md) - Deferred / circular schemas184- [Scalar](references/Scalar.md) - Lens-dependent entity fields185 - [Scalar demo](references/_ScalarDemo.md)186- [Values](references/Values.md) - Map schemas187- [All](references/All.md) - List all entities of a kind188- [Array](references/Array.md) - Immutable list schema189- [Object](references/Object.md) - Object schema190- [schema](references/schema.md) - Schema overview191- [relational-data](references/relational-data.md) - Relational data guide192- [computed-properties](references/computed-properties.md) - Computed properties guide193- [partial-entities](references/partial-entities.md) - Partial entities guide194- [side-effects](references/side-effects.md) - Side effects guide195- [sorting-client-side](references/sorting-client-side.md) - Client-side sorting guide