Derived Responses
defineDerivedResponse(...) helps when you already have a useful base response and want a more specific version of it without copying the whole definition.
This is most helpful for shared error responses.
The problem it solves
Suppose you already have a generic not-found response:
const NotFoundErrorDefinition = defineResponse({
name: "NotFoundError",
statusCode: HttpStatusCode.NOT_FOUND,
description: "Resource not found",
body: z.object({
message: z.string(),
}),
});
That is reusable, but sometimes an operation needs to add a little more detail, such as which todo was missing.
Deriving a response
const TodoNotFoundErrorDefinition = defineDerivedResponse(
NotFoundErrorDefinition,
{
name: "TodoNotFoundError",
body: z.object({
todoId: z.string(),
}),
}
);
The derived response keeps the base response's unspecified fields and merges the object body with the new fields.
In practice, the derived response still has:
- the
404status code - the original description unless you override it
- the base body fields such as
message - the added body field
todoId
How merging works
For object headers and object bodies, Typeweaver shallow-merges the base and derived Zod object schemas. Fields from the derived schema override fields with the same key in the base schema. If the body is not a z.object(...) (for example a z.string() or z.array(...)), the derived body replaces the base body entirely instead of merging.
That means this:
const UnauthorizedErrorDefinition = defineResponse({
name: "UnauthorizedError",
statusCode: HttpStatusCode.UNAUTHORIZED,
description: "Unauthorized",
header: z.object({
"WWW-Authenticate": z.string(),
}),
body: z.object({
message: z.string(),
}),
});
const TodoUnauthorizedErrorDefinition = defineDerivedResponse(
UnauthorizedErrorDefinition,
{
name: "TodoUnauthorizedError",
body: z.object({
todoId: z.string(),
}),
}
);
produces a derived response that keeps the header schema and extends the body schema.
When to use it
Use a derived response when:
- you already have a shared base response that is almost right
- you want operation-specific detail without repeating the base contract
- you want the relationship to stay obvious in the spec
When not to use it
Prefer plain shared responses when the exact same response can be reused as-is.
Prefer a fresh defineResponse(...) when the new response is meaningfully different and the inheritance would make the spec harder to read.
Clarity matters more than squeezing out a little more reuse.