Skip to content

tsmetadata/json-api

Repository files navigation

JSON API Metadata Decorators

@tsmetadata/json-api provides a standardized set of JSON:API metadata decorators for classes in TypeScript 5.2+.

By appending metadata to specific classes, class fields, and class methods, we enable out of the box support for developer tooling like ORMs, serialization, resource governance, and more. To continue, with the JSON API decorator metadata approach, you implicitly prioritize modularity and reuse.

🌱 Install

npm install @tsmetadata/json-api@latest

πŸ“‹ Feature Set

βš™οΈ Usage

Metadata Decorators

The @Resource(type: string) decorator is available and will define a resource's type (part of identification).

ex.

import { Resource } from '@tsmetadata/json-api';

@Resource('users')
class User {}

The @Id() decorator can be applied to one class field and denotes what field contains a resource's id (part of identification).

ex.

import { Id } from '@tsmetadata/json-api';

class Account {
  @Id()
  accountNumber: string;
}

The applied metadata can be retrieved using the Symbol idSymbol export.

import { idSymbol } from '@tsmetadata/json-api';

The @Attribute() decorator can be applied to many class fields and denotes what fields are resource attributes.

ex.

import { Attribute } from '@tsmetadata/json-api';

class Account {
  @Attribute()
  isPastDue: boolean;
}

The applied metadata can be retrieved using the Symbol attributesSymbol export.

import { attributesSymbol } from '@tsmetadata/json-api';

The Relationship(foreignKey: string) decorator can be applied many times to many class fields and denotes what fields are resource relationships.

The foreign key is type-safe to the field type.

ex.

import { Relationship, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

class Account {
  @Relationship('accounts')
  primaryDebtor: Customer | JSONAPIResourceLinkage;

  @Relationship('accounts')
  coDebtors: Customer[] | JSONAPIResourceLinkage;
}

class Customer {
  @Relationship('primaryDebtor')
  @Relationship('coDebtors')
  accounts: Account[] | JSONAPIResourceLinkage;
}

The applied metadata can be retrieved using the Symbol relationshipsSymbol export.

import { relationshipsSymbol } from '@tsmetadata/json-api';

The Link() decorator can be applied to many class fields and denotes what fields are resource links.

ex.

import { Link } from '@tsmetadata/json-api';

class Account {
  @Link()
  self: string;

  @Link()
  recentTransactions: string;
}

The applied metadata can be retrieved using the Symbol linksSymbol export.

import { linksSymbol } from '@tsmetadata/json-api';

The Meta() decorator can be applied to many class fields and denotes what fields are resource metadata.

ex.

import { Meta } from '@tsmetadata/json-api';

class Account {
  @Meta()
  createdAt: number;

  @Meta()
  lastUpdated: number;
}

The applied metadata can be retrieved using the Symbol metaSymbol export.

import { metaSymbol } from '@tsmetadata/json-api';

Serializers

Resource Object

The serializeResourceObject(classInstance: object) function will produce a resource object from a decorated class instance.

ex.

import { Resource, Id, Attribute, serializeResourceObject } from '@tsmetadata/json-api';

@Resource('users')
class User {
  @Id()
  customerId: string;

  @Attribute()
  active: boolean;
}

const user = new User();
user.customerId = '123';
user.active = false;

serializeResourceObject(user);

/*
  {
    "type": "users".
    "id": "123",
    "attributes": {
      "active": false
    }
  }
*/

Relationship Object

The serializeRelationshipObject(classInstance: object) function will produce a (relationship object)[https://jsonapi.org/format/#document-resource-object-relationships] from a decorated class instance.

ex.

import { Resource, Id, Link, serializeRelationshipObject } from '@tsmetadata/json-api';

@Resource('users')
class User {
  @Id()
  customerId: string;

  @Link()
  self: string;
}

const user = new User();
user.customerId = '123';
user.self = 'some-link';

serializeRelationshipObject(user);

/*
  {
    "data": {
      "type": "users",
      "id": "123"
    },
    "links": {
      "self": "some-link"
    }
  }
*/

Included Resource Objects

The serializeResourceObject(classInstance: object, keys: string[]) function will produce an array of resource objects from a decorated class instance.

ex.

import { Resource, Id, Link, serializeIncludedResourceObjects, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

// For the sake of brevity, the `Account` class definition is not included.

@Resource('users')
class User {
  @Id()
  customerId: string;

  @Relationship('primaryDebtor')
  @Relationship('coDebtors')
  accounts: Account[] | JSONAPIResourceLinkage;

  @Relationship('spouse')
  spouse: User | JSONAPIResourceLinkage;
}

const user1 = new User();
user1.customerId = '123';
user1.accounts = [someAccount, someOtherAccount];

const user2 = new User();
user2.customerId = '456';
user2.accounts = [someAccount, someOtherAccount];

serializeIncludedResourceObjects(user1, ['accounts', 'spouse']);

Deserializers

Resource Object

The deserializeResourceObject(resourceObject: JSONAPIResourceObject, cls: new (..._: any[]) => any) function will produce a class instance from a resource object.

ex.

import { Resource, Id, Attribute, serializeResourceObject, deserializeResourceObject } from '@tsmetadata/json-api';

@Resource('users')
class User {
  @Id()
  customerId: string;

  @Attribute()
  active: boolean;
}

const user = new User();
user.customerId = '123';
user.active = false;

const serializedUser = serializeResourceObject(user);

/*
  {
    "type": "users".
    "id": "123",
    "attributes": {
      "active": false
    }
  }
*/

const deserializedUser = deserializeResourceObject(user, User);

/*
  user.customerId === '123'
  user.active === false
*/

Types

Attributes Object

ex.

import type { JSONAPIAttributesObject } from '@tsmetadata/json-api';

Error Object

ex.

import type { JSONAPIErrorObject } from '@tsmetadata/json-api';

JSON API Object

ex.

import type { JSONAPIObject } from '@tsmetadata/json-api';

Link Object

ex.

import type { JSONAPILinkObject } from '@tsmetadata/json-api';

Links Object

ex.

import type { JSONAPILinksObject } from '@tsmetadata/json-api';

Meta Object

ex.

import type { JSONAPIMetaObject } from '@tsmetadata/json-api';

Pagination Links

ex.

import type { JSONAPIPaginationLinks } from '@tsmetadata/json-api';

Relationship Object

ex.

import type { JSONAPIRelationshipObject } from '@tsmetadata/json-api';

Relationships Object

ex.

import type { JSONAPIRelationshipsObject } from '@tsmetadata/json-api';

Resource Identifier Object

ex.

import type { JSONAPIResourceIdentifierObject } from '@tsmetadata/json-api';

Resource Linkage

ex.

import type { JSONAPIResourceLinkage } from '@tsmetadata/json-api';

Resource Object

ex.

import type { JSONAPIResourceObject } from '@tsmetadata/json-api';

Top Level Object

ex.

import type { JSONAPITopLevelObject } from '@tsmetadata/json-api';

😍 Full Example

import { Attribute, Link, Meta, Relationship, Resource, serializeIncludedResourceObjects,
         serializeResourceObject, deserializeResourceObject, type JSONAPIResourceLinkage } from '@tsmetadata/json-api';

@Resource('accounts')
export class Account {
  @Attribute()
  accountNumber: string;

  @Attribute()
  pastDue: boolean;

  @Relationship('accounts')
  primaryDebtor: Customer | JSONAPIResourceLinkage;

  @Relationship('accounts')
  coDebtors: Customer[] | JSONAPIResourceLinkage;

  @Link()
  self: string;

  @Meta()
  lastUpdated: number;
}

@Resource('customers')
export class Customer {
  @Id()
  id: string;

  @Attribute()
  name: string;

  @Relationship('primaryDebtor')
  @Relationship('coDebtors')
  accounts: Account[] | JSONAPIResourceLinkage;

  @Link()
  self: string;
}

const account = new Account();
account.accountNumber = '123';
account.pastDue = false;
account.coDebtors = [];
account.self = 'some-url';
account.lastUpdated = Date.now();

const customer = new Customer();
customer.id = '456';
customer.name = 'Bob';
customer.self = 'some-url';

account.primaryDebtor = customer;
customer.accounts = [account];

const serializedCustomer = serializeResourceObject(customer);

// Try logging out the results on your own!
console.log(
  serializedCustomer,
  serializeRelationshipObject(customer),
  serializeIncludedResourceObjects(customer, ['accounts'])
);

// You can deserialize too!
const customerWithResourceLinkages = deserializeResourceObject(serializedCustomer, Customer);

❓ FAQ

Q: I'm using a legacy runtime that doesn't yet support Symbol metadata.

A: You may be able to take advantage of our Symbol.metadata polyfill found here.