# Send and validate JSON

Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to send a JSON body and consume the response in one expression. Pass a type parameter when you know the response shape; pass a Standard Schema when you need runtime validation.

The examples use absolute URLs and run in the browser or Node.js. Replace `https://api.example.com` with your service's origin. Install Ky in your project:

```bash
npm install ky
```

## 1. Send JSON and read the response

Send an array when your endpoint accepts a batch of records; see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for basic JSON serialization and headers.

```ts
import ky from 'ky';

async function main() {
	const result = await ky.post('https://api.example.com/users', {
		json: [{name: 'Ada'}, {name: 'Grace'}],
	}).json();

	console.log(result);
}

main().catch(console.error);
```

This sends two records in one JSON array and logs your service's parsed response; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the single-record example and the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for shortcut behavior and the default response type.

## 2. Give the response a TypeScript type

Use `.json<User>()` to get a `Promise<User>`. The type parameter describes your application data; it does not validate the response at runtime.

```ts
import ky from 'ky';

type User = {
	name: string;
};

async function main() {
	const user = await ky.get('https://api.example.com/users/1').json<User>();

	console.log(user.name);
}

main().catch(console.error);
```

TypeScript lets you access `user.name` as a string. You can also put the type parameter on the request, as in `ky.get<User>(url).json()`. Use a schema instead when you need to check what the server actually sends.

## 3. Validate the response with a schema

Pass a Standard Schema compatible validator, such as Zod 3.24+, to `.json(schema)` to get the schema's validated output; see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for handling [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror).

Install Zod for this example:

```bash
npm install zod
```

```ts
import ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';

const userSchema = z.object({name: z.string()});

async function main() {
	try {
		const result = await ky.get('https://api.example.com/users/1').json(userSchema);
		const user = userSchema.parse(result);
		console.log(user.name);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			console.error(error.issues);
		} else {
			throw error;
		}
	}
}

main().catch(console.error);
```

With a valid response, `user.name` is a string inferred from the schema. With an invalid response, the catch block logs the issues instead. For handling request failures alongside schema rejection, see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors).

## 4. Customize serialization and parsing

Use `stringifyJson` to change the outgoing representation with a replacer. This example omits `internalNote` from the serialized body while retaining `name`:

```ts
import ky from 'ky';

async function main() {
	const result = await ky.post('https://api.example.com/users', {
		json: {name: 'Ada', internalNote: 'Local draft'},
		stringifyJson: data => {
			const text = JSON.stringify(data, (key, value: unknown) =>
				key === 'internalNote' ? undefined : value,
			);
			if (text === undefined) {
				throw new TypeError('The request value cannot be serialized to JSON');
			}
			return text;
		},
	}).json();

	console.log(result);
}

main().catch(console.error);
```

Use `parseJson` to change how Ky consumes response text. It receives the text and a context object containing `request` and `response`. Calling `.json()` without a schema on an empty response body throws because the body cannot be parsed. Provide custom empty-body handling with `parseJson`:

```ts
import ky from 'ky';

async function main() {
	const result = await ky.get('https://api.example.com/users/1', {
		parseJson: (text, {request, response}) => {
			console.log(`Parsing JSON from ${request.url} (status: ${response.status})`);
			if (text.trim() === '') {
				return null;
			}
			const parsed: unknown = JSON.parse(text);
			return parsed;
		},
	}).json();

	console.log(result);
}

main().catch(console.error);
```

This returns `null` for an empty or whitespace-only body and parsed JSON otherwise. Inspect the URL and status in your own request's log. When you also pass a schema to `.json(schema)`, Ky runs `parseJson` before validation, so the schema must accept the value your parser returns.

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `json` | `unknown` | Not set | Serializes an outgoing value into the request body. Use a value accepted by `JSON.stringify()`. |
| `stringifyJson` | `(data: unknown) => string` | `JSON.stringify()` | Replaces outgoing JSON serialization. |
| `parseJson` | `(text: string, context: {request: Request; response: Response}) => unknown` | `JSON.parse()` | Replaces incoming JSON parsing, including custom empty-body handling. |

For JSON content-type headers, see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start).

## Related

- [Ky quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) — Make your first request.
- [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) — Reuse request options across calls.
- [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) — Handle HTTP, network, timeout, and schema errors.
