# 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

```sh
npm install zod-stream zod openai
```

`schema-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

```ts
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:

```ts
type CompletionMeta = {
  _isValid: boolean
  _activePath: (string | number | undefined)[]
  _completedPaths: (string | number | undefined)[][]
}
```

## Schema stubs

```ts
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

```ts
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:

```ts
{
  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>`.

```ts
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

1. Upgrade to Zod 4 and OpenAI 6.
2. Remove `zod-to-json-schema`; it is no longer used.
3. Treat progressive chunks as `ZodStreamChunk<T>`, not `Partial<z.output<T>>`.
4. Update `JSON_SCHEMA` snapshots and middleware for the `type: "json_schema"` payload.
5. Pass `client` explicitly to `createAgent` and handle final `ZodError` failures.
6. Keep `FUNCTIONS` only for providers that still implement the deprecated fields.

For Zod 3 progressive parsing without OpenAI response-model conversion, use `schema-stream` 4 directly.
