Skip to content

Parsing Request Bodies ​

This library provides robust tools for validating and extracting data from incoming JSON:API resource and relationship requests. The goal is to ensure strong typing, clean separation of concerns, and helpful, standard-compliant error reporting; all while maintaining runtime performance.

  • include
  • fields
  • sort
  • filter
  • page

Overview ​

Body parsing includes:

  • Enforcing content-type requirements (application/vnd.api+json)
  • Decoding and validating the JSON structure
  • Ensuring resource identifiers, types, attributes, and relationships match expected schemas
  • Optional validation of included resources and relationships
  • Returning parsed and typed resource data in a usable form

Input Context ​

To parse a body, you must provide a BodyContext:

ts
type BodyContext = {
    body: Record<string, unknown> | string;
    contentType: string;
};

The body may be a raw JSON string (e.,g. from req.body) or an already-parsed object.

Content Type Validation ​

When parsing a request, it is automatically ensured that the Content-Type header:

  • Is present and equals application/vnd.api+json
  • Does not contain unknown media type parameters

Invalid content types throw a JsonApiError with status 415.

Parsing Resource Requests ​

Use parseResourceRequest to handle resource creation and update requests, typically for endpoints like POST /articles or PATCH /articles/:id.

You must specify the expected resource type, and optionally provide Zod schemas for:

  • the resource id (for PATCH and PUT)
  • attributes
  • relationships
  • any included resource types

Example ​

ts
import { 
    parseResourceRequest,
    relationshipSchema,
    resourceIdentifierSchema,
} from "@jsonapi-serde/server/request";
import { z } from "zod/v4";

const result = parseResourceRequest(context, {
    type: "article",
    idSchema: z.uuid(),
    attributesSchema: z.strictObject({
        title: z.string(),
        content: z.string(),
    }),
    relationshipsSchema: z.strictObject({
        author: relationshipSchema(resourceIdentifierSchema("person")),
    }),
});

This returns a typed result:

ts
type Result = {
    id: string;
    type: "article";
    attributes: {
        title: string;
        content: string;
    };
    relationships: {
        author: {
            data: { type: "person", id: string };
        };
    };
};

Relationships ​

Relationships can be defined as to-one, nullable to-one or to-many:

To-One ​

ts
relationshipSchema(resourceIdentifierSchema("person"))

Nullable To-One ​

ts
relationshipSchema(resourceIdentifierSchema("person").nullable())

To-Many ​

ts
relationshipSchema(resourceIdentifierSchema("person").array())

Included resources ​

A client may include related resources in the request using the top-level included array. These included resources can be validated using the includedTypeSchemas option in parseResourceRequest.

Each entry in includedTypeSchemas can provide at an attributesSchema and a relationshipsSchema. All included resources must have a lid (local identifier) so they can be referenced from the main data relationships.

Example payload with included ​

json
{
    "data": {
        "type": "article",
        "attributes": { "title": "My Post", "content": "..." },
        "relationships": {
            "author": {
                "data": { "type": "person", "lid": "temp-1" }
            }
        }
    },
    "included": [
        {
            "type": "person",
            "lid": "temp-1",
            "attributes": { "name": "Jane Doe" }
        }
    ]
}

To validate this, use clientResourceIdentifierSchema() to allow lid-based references:

ts
import { 
    parseResourceRequest,
    relationshipSchema,
    clientResourceIdentifierSchema,
} from "@jsonapi-serde/server/request";

const result = parseResourceRequest(context, {
    type: "article",
    attributesSchema: z.object({
        title: z.string(),
        content: z.string(),
    }),
    relationshipsSchema: z.object({
        author: relationshipSchema(clientResourceIdentifierSchema("person")),
    }),
    includedTypeSchemas: {
        person: {
            attributesSchema: z.object({
                name: z.string(),
            }),
        },
    },
});

This setup allows author.data to refer to a person by lid, and ensures that the included person resource is present and valid.

Supporting Both Server and Client Identifiers ​

If you want to support both client-provided local identifiers (lid) and traditional server-side identifiers (id), you can use a union of resourceIdentifierSchema and clientResourceIdentifierSchema:

ts
relationshipSchema(
    resourceIdentifierSchema("person").or(
        clientResourceIdentifierSchema("person")
    )
)

This allows the relationship to use either:

json
{ "type": "person", "id": "abc-123" }

or

json
{ "type": "person", "lid": "temp-1" }

whichever the client provides. This is useful for cases where you allow both new and existing related resources in the same API call.

Parsing Relationship Requests ​

Use parseRelationshipRequest to handle relationship updates or deletions, such as:

  • PATCH /articles/1/relationships/author (to-one)
  • PATCH /articles/1/relationships/tags (to-many)
  • DELETE /articles/1/relationships/editor (removal)

Refer to the JSON:API specificaiton for relationship update semantics.

To-One: parseRelationshipRequest ​

Parses a to-one relationship request and returns a validated ID or null.

ts
const authorId = parseRelationshipRequest(context, "person");
// authorId: string

With ID Validation ​

ts
const editorId = parseRelationshipRequest(context, "person", z.uuid());
// editorId: string (validated as UUID)

Nullable Relationships ​

To allow unsetting the relationship, use .nullable():

ts
const editorId = parseRelationshipRequest(context, "person", z.uuid().nullable());
// editorId: string | null

Example Request Body ​

json
{
    "data": {
        "type": "person",
        "id": "f27aa413-8ab2-4981-9351-e8e7c2244795"
    }
}

Unsetting the relationship:

json
{
    "data": null
}

To-Many: parseRelationshipsRequest ​

ts
const tagIds = parseRelationshipsRequest(context, "tag");
// tagIds: string[]

With ID Validation ​

ts
const tagIds = parseRelationshipsRequest(context, "tag", z.uuid());
// tagIds: string[]

Example Request Body ​

json
{
    "data": [
        { "type": "tag", "id": "a1" },
        { "type": "tag", "id": "b2" }
    ]
}