yayson
v4.0.0
Published
A library for serializing and reading JSON API standardized data in JavaScript.
Readme
YAYSON
A library for serializing and reading JSON API data in JavaScript.
YAYSON supports both ESM and CommonJS, has zero dependencies, and works in the browser and in Node.js 20+.
Installing
npm install yaysonUpgrading from 3.x
Version 4.x is a full TypeScript rewrite with several new features and breaking changes.
New features
- TypeScript support — Full type definitions included, with type inference from schema registries
- Schema validation — Optional runtime validation using Zod or any compatible library with
parse()/safeParse()methods - Dual ESM/CJS package — Native ES module support alongside CommonJS
yayson/utilsentry point — Helper functions (getType,getLinks,getMeta,getRelationshipLinks,getRelationshipMeta) for reading model metadataretrieveAll(type, data)— Sync data and return only models of a specific typesyncAll()— Always returns an array (useful when you always want array behavior)fieldsproperty — Limit which attributes a Presenter includes in its output
Breaking changes
Node.js version
Node.js 20+ is now required (was 14+).
Metadata uses Symbols instead of plain properties
In 3.x, resource type, links, and meta were stored as plain properties on models (model.type, model.links, model.meta). Relationship metadata used model._links and model._meta.
In 4.x, these use Symbol keys to avoid collisions with your data. Use the helpers from yayson/utils:
// 3.x
model.type
model.links
model.meta
relatedModel._links
relatedModel._meta
// 4.x
import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
getType(model)
getLinks(model)
getMeta(model)
getRelationshipLinks(relatedModel)
getRelationshipMeta(relatedModel)sync() restores 3.x behavior
sync() now matches 3.x behavior: single resources return a model directly, collections return an array. If you always want an array, use syncAll().
// Single resource — returns model directly
const event = store.sync({ data: { type: 'events', id: '1', attributes: { name: 'Demo' } } })
event.name // 'Demo'
// Collection — returns array
const events = store.sync({ data: [{ type: 'events', id: '1', attributes: { name: 'Demo' } }] })
events.length // 1
// Always returns array
const events = store.syncAll(data)
// retrieve/retrieveAll also available
const event = store.retrieve('events', data)
const events = store.retrieveAll('events', data)Document-level meta uses Symbols
In 3.x, document-level metadata was stored as result.meta. In 4.x, it uses a Symbol key:
// 3.x
const result = store.sync(data)
result.meta // { total: 100 }
// 4.x
import { META } from 'yayson/utils'
const result = store.sync(data)
result[META] // { total: 100 }Presenting data
A basic Presenter can look like this:
// ESM
import yayson from 'yayson'
const { Presenter } = yayson()
// CommonJS
const yayson = require('yayson')
const { Presenter } = yayson()
class BikePresenter extends Presenter {
static type = 'bikes'
}
const bike = {
id: 5,
name: 'Monark',
}
BikePresenter.render(bike)This would produce:
{
data: {
id: '5',
type: 'bikes',
attributes: {
name: 'Monark'
}
}
}
It also works with arrays, so if you send an array to render, "data" will be an array.
A bit more advanced example:
import yayson from 'yayson'
const { Presenter } = yayson()
class WheelPresenter extends Presenter {
static type = 'wheels'
relationships() {
return { bike: BikePresenter }
}
}
class BikePresenter extends Presenter {
static type = 'bikes'
relationships() {
return { wheels: WheelPresenter }
}
}Filtering attributes with fields
Use the static fields property to limit which attributes are included in the output:
class UserPresenter extends Presenter {
static type = 'users'
static fields = ['name', 'email', 'createdAt']
}
const user = {
id: 1,
name: 'John',
email: '[email protected]',
password: 'secret',
createdAt: '2024-01-01',
}
UserPresenter.render(user)This would produce:
{
data: {
id: '1',
type: 'users',
attributes: {
name: 'John',
email: '[email protected]',
createdAt: '2024-01-01'
}
}
}The password field is excluded because it's not in the fields array. This is useful for hiding sensitive data or reducing payload size without overriding the attributes() method.
Sequelize support
By default it is set up to handle standard JS objects. You can also make it handle Sequelize.js models like this:
import yayson from 'yayson'
const { Presenter } = yayson({ adapter: 'sequelize' })You can also define your own adapter globally:
import yayson from 'yayson'
const { Presenter } = yayson({
adapter: {
id: function (model) {
return 'omg' + model.id
},
get: function (model, key) {
return model[key]
},
},
})Take a look at the SequelizeAdapter if you want to extend YAYSON to your ORM. Pull requests are welcome. :)
render() options
The second argument to render() accepts an options object with meta and links:
ItemsPresenter.render(items, {
meta: { total: 100, page: 1 },
links: {
self: 'http://example.com/items?page=1',
next: 'http://example.com/items?page=2',
},
})This would produce:
{
meta: {
total: 100,
page: 1
},
links: {
self: 'http://example.com/items?page=1',
next: 'http://example.com/items?page=2'
},
data: [...]
}Both meta and links are optional and add top-level properties to the JSON API document.
Parsing data
You can use a Store like this:
import yayson from 'yayson'
const { Store } = yayson()
const store = new Store()
const data = await adapter.get({ path: '/events/' + id })
const event = store.sync(data) // single resource → model directlyThe sync() method returns a single model for single resources and an array for collections. Use syncAll() if you always want an array.
const allSynced = store.syncAll(data) // always returns arrayFiltering by type with retrieveAll
Use retrieveAll() to sync data and return only models of a specific type:
const data = await adapter.get({ path: '/events/' })
// Sync and return only events (filters out included resources)
const events = store.retrieveAll('events', data)This is useful when the response contains multiple types but you only need the primary resources:
// Response contains events and their related images
const response = {
data: [
{ type: 'events', id: '1', attributes: { name: 'Conference' } },
{ type: 'events', id: '2', attributes: { name: 'Meetup' } },
],
included: [{ type: 'images', id: '10', attributes: { url: 'http://example.com/img.jpg' } }],
}
// Get only the events, not the included images
const events = store.retrieveAll('events', response)
// events.length === 2Finding synced data
You can also find in previously synced data:
const event = store.find('events', id)
const images = store.findAll('images')Schema Validation and Type Inference
YAYSON supports optional schema validation using Zod or any compatible schema library. This enables:
- Runtime validation: Ensure your data matches expected schemas
- TypeScript type inference: Get full type safety without manual type definitions
- Strict or safe modes: Throw errors or collect validation issues
Basic Usage with Zod
import { z } from 'zod'
import yayson from 'yayson'
const { Store } = yayson()
const eventSchema = z
.object({
id: z.string(),
name: z.string(),
date: z.string(),
})
.passthrough()
const schemas = {
events: eventSchema,
} as const
const store = new Store({ schemas, strict: true })
store.sync({
data: {
type: 'events',
id: '1',
attributes: { name: 'TypeScript Meetup', date: '2025-01-15' },
},
})
// TypeScript infers the correct type automatically
const event = store.find('events', '1')
// event.name, event.date are fully typed!Validation Modes
Strict mode (throws errors on validation failure):
const store = new Store({ schemas, strict: true })Safe mode (collects errors without throwing):
const store = new Store({ schemas, strict: false })
store.sync(invalidData)
if (store.validationErrors.length > 0) {
console.warn('Validation issues:', store.validationErrors)
}Schemas must be Zod-like objects with parse() and safeParse() methods. Any library that provides this interface will work.
Utilities
YAYSON stores metadata on models using Symbol keys. The yayson/utils entry point provides helpers for reading them:
import { getType, getLinks, getMeta, getRelationshipLinks, getRelationshipMeta } from 'yayson/utils'
const events = store.syncAll(data)
const event = events[0]
getType(event) // 'events'
getLinks(event) // { self: 'http://...' }
getMeta(event) // { createdBy: 'admin' }
getRelationshipLinks(event.image) // { self: 'http://.../relationships/image' }
getRelationshipMeta(event.image) // { permission: 'read' }The raw symbols (TYPE, LINKS, META, REL_LINKS, REL_META) are also exported from yayson/utils if you prefer direct access.
Note that syncAll() and retrieveAll() return arrays with a META symbol property for document-level metadata. Array methods like .filter() and .map() return plain arrays without this property, so extract it before transforming:
import { META } from 'yayson/utils'
const result = store.syncAll(data)
const meta = result[META] // { total: 100, page: 1 }
const filtered = result.filter((e) => e.name === 'Demo')
// filtered[META] is undefined — use the extracted `meta` insteadClaude Code skill
YAYSON includes a Claude Code skill that helps you write Presenters and set up Stores. To install it, copy the skill/yayson directory to your project's .claude/skills/ or your personal ~/.claude/skills/:
# Project-level (available to everyone working on that project)
cp -r node_modules/yayson/skill/yayson .claude/skills/
# Personal (available across all your projects)
cp -r node_modules/yayson/skill/yayson ~/.claude/skills/Use in the browser
Recommended way is to use it via webpack or similar build system wich lets you just require the package as usual.
Legacy support
Earlier versions of JSON API worked a bit different from 1.0. Therefore YAYSON provides legacy presenters and stores in order to have interoperability between the versions.
Basic Usage
// ESM
import yayson from 'yayson/legacy'
const { Presenter, Store } = yayson()
// CommonJS
const yayson = require('yayson/legacy')
const { Presenter, Store } = yayson()const store = new Store({
types: {
events: 'event',
images: 'image',
},
})
const event = store.sync({
event: { id: '1', name: 'Demo Event' },
})
// single resource → returns model directly
const allSynced = store.syncAll({
event: { id: '1', name: 'Demo Event' },
images: [{ id: '2', url: 'http://example.com/image.jpg' }],
})
// syncAll always returns array
const event = store.find('event', '1')Filtering by type with retrieveAll
Use retrieveAll() to sync data and return only models of a specific type:
const events = store.retrieveAll('event', {
event: [
{ id: '1', name: 'Event 1' },
{ id: '2', name: 'Event 2' },
],
image: [{ id: '3', url: 'http://example.com/image.jpg' }],
})
// events.length === 2 (only events, not images)Options can be passed when creating the store:
const store = new Store({
types: { events: 'event' },
})Schema Validation for Legacy Store
The legacy store also supports schema validation and type inference, maintaining full backward compatibility:
import yayson from 'yayson/legacy'
import { z } from 'zod'
const { Store } = yayson()
const eventSchema = z.object({
id: z.string(),
name: z.string(),
date: z.string(),
})
const store = new Store({
schemas: { event: eventSchema },
strict: true,
})
store.sync({ event: { id: '1', name: 'Event', date: '2025-01-15' } })
// TypeScript infers the correct type
const event = store.find('event', '1')
// event.name, event.date are fully typed!Validation with Type Mapping
You can combine type mapping with schemas:
const store = new Store({
types: {
events: 'event', // Map plural to singular
},
schemas: {
event: eventSchema, // Schema uses normalized type
},
strict: false, // Safe mode
})
store.sync({ events: [{ id: '1', name: 'Event' }] })
if (store.validationErrors.length > 0) {
console.warn('Validation issues:', store.validationErrors)
}Validation with Relations
Schemas validate the complete model after relations are resolved:
const eventSchema = z.object({
id: z.string(),
name: z.string(),
images: z.array(
z.object({
id: z.string(),
url: z.string(),
}),
),
})
const imageSchema = z.object({
id: z.string(),
url: z.string(),
})
const store = new Store({
schemas: { event: eventSchema, image: imageSchema },
strict: true,
})
store.sync({
links: {
'event.images': { type: 'image' },
},
event: { id: '1', name: 'Event', images: ['2'] },
image: [{ id: '2', url: 'http://example.com/image.jpg' }],
})
const event = store.find('event', '1')
// Relations are resolved before validation
// event.images is an array of validated image objectsNote: Validation happens eagerly during sync() when schemas are configured. This allows you to check store.validationErrors immediately after syncing.

