data-client-schema
reactive/data-client/.agents/skills/data-client-schema/SKILL.md
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.
What's in it
- 1. Defining Schemas
- Object
- List
- Map
- Lens-dependent entity fields
- Derived / selector pattern
- 2. Entity best practices
- 3. Entity lifecycle methods
- 4. Union Types (Polymorphic Schemas)
- 5. Collections (Mutable Lists & Maps)
- pk routing
- nonFilterArgumentKeys
- Extenders
- 6. Supplementary Endpoints (enrich existing entities)
- 7. Best Practices & Notes
- 8. Common Mistakes to Avoid
- References
---
name: data-client-schema
description: 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.
license: Apache 2.0
---
## 1. Defining Schemas
Define [schemas](references/schema.md) to represent the JSON returned by an endpoint. Compose these
to represent the data expected.
### Object
- [Entity](references/Entity.md) - represents a single unique object (denormalized)
- [EntityMixin](references/EntityMixin.md) - turn any pre-existing class into an Entity
- [new Union(Entity)](references/Union.md) - polymorphic objects (A | B)
- [`{[key:string]: Schema}`](references/Object.md) - immutable objects
- [new Invalidate(Entity|Union)](references/Invalidate.md) - to delete an Entity
- [new Lazy(() => Schema)](references/Lazy.md) - break circular imports / defer deep recursive denormalization
### List
- [new Collection([Schema])](references/Collection.md) - mutable/growable lists
- [`[Schema]`](references/Array.md) - immutable lists
- [new All(Entity|Union)](references/All.md) - list all Entities of a kind
### Map
- `new Collection(Values(Schema))` - mutable/growable maps
- [new Values(Schema)](references/Values.md) - immutable maps
### Lens-dependent entity fields
- [new Scalar({ lens, key, entity? })](references/Scalar.md) - fields that vary by runtime lens (portfolio, currency, locale) without entity mutation
### Derived / selector pattern
- [new Query(Queryable)](references/Query.md) - memoized programmatic selectors
```ts
const queryRemainingTodos = new Query(
TodoResource.getList.schema,
entries => entries.filter(todo => !todo.completed).length,
);
```
```ts
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()](references/validation.md) → **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()](references/validation.md) → `fromJS()` → **unvisit nested schemas** (recurse into `schema` fields).
---
## 4. **Union Types (Polymorphic Schemas)**
To define polymorphic resources (e.g., events), use [Union](references/Union.md) and a discriminator field.
```typescript
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](references/Collection.md) 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:
```ts
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[]`:
```ts
new Collection([Todo], { nonFilterArgumentKeys: /orderBy|sortDir/ })
```
### Extenders
All usable with `ctrl.set()` (local-only) or via [RestEndpoint extenders](https://dataclient.io/rest/api/RestEndpoint) (network).
| Method | Type | Description |
|--------|------|-------------|
| `push` | Array | Entity | Append items to end |
| `unshift` | Array | Entity | Prepend items to start |
| `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](references/partial-entities.md) 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
Vue projects: read `<name>.vue.md` instead of `<name>.md` when it exists.
For detailed API documentation, see the [references](references/) directory:
- [Entity](references/Entity.md) - Normalized data class
- [EntityMixin](references/EntityMixin.md) - Turn any class into an Entity
- [Collection](references/Collection.md) - Mutable/growable lists
- [Union](references/Union.md) - Polymorphic schemas
- [Query](references/Query.md) - Programmatic selectors
- [Invalidate](references/Invalidate.md) - Delete entities
- [Lazy](references/Lazy.md) - Deferred / circular schemas
- [Scalar](references/Scalar.md) - Lens-dependent entity fields
- [Scalar demo](references/_ScalarDemo.md)
- [Values](references/Values.md) - Map schemas
- [All](references/All.md) - List all entities of a kind
- [Array](references/Array.md) - Immutable list schema
- [Object](references/Object.md) - Object schema
- [schema](references/schema.md) - Schema overview
- [relational-data](references/relational-data.md) - Relational data guide
- [computed-properties](references/computed-properties.md) - Computed properties guide
- [partial-entities](references/partial-entities.md) - Partial entities guide
- [side-effects](references/side-effects.md) - Side effects guide
- [sorting-client-side](references/sorting-client-side.md) - Client-side sorting guide
More agent context in reactive/data-client
28 other files this repository gives its agents.
Cursor rule
Skill
- changeset.agents/skills/changeset/SKILL.md
- data-client-endpoint-setup.agents/skills/data-client-endpoint-setup/SKILL.md
- data-client-graphql-setup.agents/skills/data-client-graphql-setup/SKILL.md
- data-client-manager.agents/skills/data-client-manager/SKILL.md
- data-client-react.agents/skills/data-client-react/SKILL.md
- data-client-react-testing.agents/skills/data-client-react-testing/SKILL.md
- data-client-rest-setup.agents/skills/data-client-rest-setup/SKILL.md
- data-client-rest.agents/skills/data-client-rest/SKILL.md
- data-client-setup.agents/skills/data-client-setup/SKILL.md
- data-client-v0.18-migration.agents/skills/data-client-v0.18-migration/SKILL.md
- data-client-vue.agents/skills/data-client-vue/SKILL.md
- data-client-vue-testing.agents/skills/data-client-vue-testing/SKILL.md
- frontend-design.agents/skills/frontend-design/SKILL.md
- initialize.agents/skills/initialize/SKILL.md
- interface-design.agents/skills/interface-design/SKILL.md
- packages-documentation.agents/skills/packages-documentation/SKILL.md
- path-to-regexp-v8-migration.agents/skills/path-to-regexp-v8-migration/SKILL.md
- pr.agents/skills/pr/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

