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.
includefieldssortfilterpage
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
includedresources and relationships - Returning parsed and typed resource data in a usable form
Input Context
To parse a body, you must provide a BodyContext:
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(forPATCHandPUT) attributesrelationships- any
includedresource types
Example
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:
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
relationshipSchema(resourceIdentifierSchema("person"))Nullable To-One
relationshipSchema(resourceIdentifierSchema("person").nullable())To-Many
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
{
"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:
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:
relationshipSchema(
resourceIdentifierSchema("person").or(
clientResourceIdentifierSchema("person")
)
)This allows the relationship to use either:
{ "type": "person", "id": "abc-123" }or
{ "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.
const authorId = parseRelationshipRequest(context, "person");
// authorId: stringWith ID Validation
const editorId = parseRelationshipRequest(context, "person", z.uuid());
// editorId: string (validated as UUID)Nullable Relationships
To allow unsetting the relationship, use .nullable():
const editorId = parseRelationshipRequest(context, "person", z.uuid().nullable());
// editorId: string | nullExample Request Body
{
"data": {
"type": "person",
"id": "f27aa413-8ab2-4981-9351-e8e7c2244795"
}
}Unsetting the relationship:
{
"data": null
}To-Many: parseRelationshipsRequest
const tagIds = parseRelationshipsRequest(context, "tag");
// tagIds: string[]With ID Validation
const tagIds = parseRelationshipsRequest(context, "tag", z.uuid());
// tagIds: string[]Example Request Body
{
"data": [
{ "type": "tag", "id": "a1" },
{ "type": "tag", "id": "b2" }
]
}