zod-stream
v4.0.0
Published
A client for node or the browser to generate and consume streaming json
Maintainers
Readme
zod-stream
zod-stream turns chunked JSON into progressive, schema-shaped values with validation and completion-path metadata. It keeps SchemaStream's early partial emissions while adding Zod 4 validation and OpenAI Chat Completions helpers.
Requirements
- Zod 4 (
zod@^4.0.0) - OpenAI 6 (
openai@^6.0.0) when using the OpenAI helpers - Node.js 20 or newer for the OpenAI 6 client; browser and Bun streams are also supported
npm install zod-stream zod openaischema-stream itself supports Zod 3.25 and Zod 4. zod-stream 4 is intentionally Zod 4-only because its response-model conversion uses Zod 4's native z.toJSONSchema API and its public types use Zod 4 input/output semantics.
Progressive streaming
import ZodStream, { isPathComplete } from "zod-stream"
import { z } from "zod"
const schema = z.object({
title: z.string(),
details: z.object({ count: z.number() }),
items: z.array(z.object({ label: z.string() }))
})
const client = new ZodStream()
const stream = await client.create({
response_model: { schema },
completionPromise: async () => {
const response = await fetch("/api/extract")
if (!response.body) throw new Error("Missing response body")
return response.body
}
})
for await (const chunk of stream) {
if (isPathComplete(["details", "count"], chunk)) {
console.log(chunk.details?.count)
}
console.log(chunk._meta)
}Each ZodStreamChunk<T> is a recursively partial representation of z.input<T>. Primitive leaves may be null until their JSON arrives, nested object fields may be incomplete, and transforms have not run. This is deliberately not typed as Partial<z.output<T>>.
The generator validates the completed input before it finishes and rejects with the original parser, source-stream, or ZodError failure. Its generator return value is the validated z.output<T>; normal for await consumers can use _isValid and validate or retain their own final value, while stream-hooks captures that return value for onEnd.
_meta is reserved for stream metadata:
type CompletionMeta = {
_isValid: boolean
_activePath: (string | number | undefined)[]
_completedPaths: (string | number | undefined)[][]
}Schema stubs
const stub = client.getSchemaStub({
schema,
defaultData: { details: { count: 0 } }
})Stubs use schema defaults, explicit defaults, nested objects, empty arrays/records, and configurable primitive placeholders from SchemaStream.
OpenAI response models
import OpenAI from "openai"
import { OAIStream, withResponseModel } from "zod-stream"
const openai = new OpenAI()
const params = withResponseModel({
response_model: {
schema,
name: "Extract_details",
description: "Extract the requested details"
},
mode: "JSON_SCHEMA",
params: {
model: "gpt-4o-mini",
messages: [{ role: "user", content: "Extract this..." }]
}
})
const completion = await openai.chat.completions.create({ ...params, stream: true })
const bytes = OAIStream({ res: completion })JSON_SCHEMA now emits OpenAI's current structured-output shape:
{
response_format: {
type: "json_schema",
json_schema: { name, description, schema, strict: true }
}
}The JSON Schema is generated by z.toJSONSchema(schema, { target: "draft-07", io: "input" }). Object schemas are recursively closed with additionalProperties: false for OpenAI strict mode. Zod types that cannot be represented as JSON Schema, such as z.date(), throw during parameter construction. OpenAI strict mode supports only a JSON Schema subset, so provider rejection remains possible for schemas outside that subset.
Response modes
| Mode | Behavior |
| --- | --- |
| JSON_SCHEMA | Current OpenAI structured outputs with native Zod 4 JSON Schema |
| TOOLS | Forces the generated function tool and preserves existing function/custom tools |
| JSON | Uses legacy { type: "json_object" } plus a schema system message |
| MD_JSON | Requests schema-conforming JSON through a system message |
| THINKING_MD_JSON | Retains the existing thinking-tag/markdown compatibility prompt |
| FUNCTIONS | Retains deprecated OpenAI functions/function_call compatibility |
Prefer JSON_SCHEMA for models that support structured outputs or TOOLS where tool calling is required. FUNCTIONS, JSON, and the markdown modes remain public compatibility surfaces; they are not silently redirected.
createAgent
createAgent remains a Chat Completions helper. It now requires an explicit OpenAI 6 client, validates non-streaming JSON through the response schema, and returns z.output<T>.
import OpenAI from "openai"
import { createAgent } from "zod-stream"
const agent = createAgent({
client: new OpenAI(),
mode: "JSON_SCHEMA",
response_model: { schema, name: "Extract_details" },
defaultClientOptions: {
model: "gpt-4o-mini",
messages: []
}
})
const result = await agent.completion({
messages: [{ role: "user", content: "Extract this..." }]
})The Responses API is not used internally in this major so existing Chat Completions streaming and parsing behavior remains available.
Migrating from 3.x
- Upgrade to Zod 4 and OpenAI 6.
- Remove
zod-to-json-schema; it is no longer used. - Treat progressive chunks as
ZodStreamChunk<T>, notPartial<z.output<T>>. - Update
JSON_SCHEMAsnapshots and middleware for thetype: "json_schema"payload. - Pass
clientexplicitly tocreateAgentand handle finalZodErrorfailures. - Keep
FUNCTIONSonly for providers that still implement the deprecated fields.
For Zod 3 progressive parsing without OpenAI response-model conversion, use schema-stream 4 directly.
