# Send JSON and read responses

Use [`ky`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#default) to put a JSON value in a request and read the response directly as JSON. Choose a TypeScript type when the response shape is trusted, or pass a Standard Schema when the response must be checked at runtime.

## Send a JSON request

Pass the value to the `json` option instead of serializing it into `body`. Ky serializes the value and sets `Content-Type: application/json` unless you provide that header yourself. The request shortcut returns a response promise, so you can call `json()` without first awaiting a raw response.

```ts
import ky from 'ky';

interface NewUser {
	name: string;
	email: string;
}

interface User extends NewUser {
	id: string;
}

const newUser: NewUser = {
	name: 'Ada Lovelace',
	email: 'ada@example.com',
};

const createUser = async (): Promise<User> => ky
	.post<User>('https://api.example.com/users', {json: newUser})
	.json();

const showCreatedUser = async (): Promise<void> => {
	const user = await createUser();
	console.log(user.id, user.name);
};

void showCreatedUser();
```

The server receives a JSON object with `name` and `email`. The response is parsed as JSON and `user` has the compile-time type `User`; the type parameter does not validate the server's data at runtime.

## Read a typed JSON response

`.json()` defaults to `unknown`. Give the request or the body method a type parameter when you want a typed result:

```ts
import ky from 'ky';

interface User {
	id: string;
	name: string;
}

const showTypedUsers = async (): Promise<void> => {
	const fromRequestType = await ky<User>('https://api.example.com/users/1').json();
	const fromBodyMethodType = await ky('https://api.example.com/users/1').json<User>();

	console.log(fromRequestType.name, fromBodyMethodType.id);
};

void showTypedUsers();
```

Both calls produce a parsed JSON object typed as `User`. The body method is a direct shortcut; it also sets an appropriate `Accept` header for JSON. A non-2xx response rejects instead of producing a successful value.

## Validate the response with a schema

Install a Standard Schema-compatible validator such as Zod alongside Ky:

```bash
npm install ky zod
```

Pass the schema to `.json(schema)`. Ky returns the validator's inferred output only after validation succeeds. A rejected schema produces [`SchemaValidationError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#schemavalidationerror), not a Ky HTTP lifecycle error.

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

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

const createUser = async (): Promise<void> => {
	try {
		const user = await ky.post('https://api.example.com/users', {
			json: {name: 'Ada Lovelace'},
		}).json(userSchema);
		console.log(user.id, user.name);
	} catch (error) {
		if (error instanceof SchemaValidationError) {
			console.error('The response shape is invalid', error.issues);
			return;
		}

		throw error;
	}
};

const run = async (): Promise<void> => createUser();

void run();
```

This combines a JSON request with runtime validation of the JSON response: the request sends `name`, and the call returns a value only when the response also contains a string `id` and `name`.

## Handle HTTP failures separately

For non-2xx responses and Ky's [`HTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-httperror#httperror) and [`isHTTPError`](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky#ishttperror) handling, see [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses).

```ts
import ky, {isHTTPError} from 'ky';

const loadUser = async (): Promise<void> => {
	try {
		const user = await ky('https://api.example.com/users/1').json<{name: string}>();
		console.log(user.name);
	} catch (error) {
		if (isHTTPError(error)) {
			console.error('Request failed', error.response.status, error.data);
			return;
		}

		throw error;
	}
};

const run = async (): Promise<void> => loadUser();

void run();
```

`error.response` remains available for status and headers. If no response arrives, the failure is not an `HTTPError`; handle network failures separately when your application needs that distinction.

## Options that matter here

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `json` | any value accepted by `JSON.stringify()` | not set | Serializes the value into the request body and sets `Content-Type: application/json` unless your `headers` override it. |
| `headers` | Fetch headers | not set | Supplies request headers; an explicit `Content-Type` takes precedence over Ky's JSON header. |
| `throwHttpErrors` | boolean | enabled | Controls whether non-2xx responses reject as `HTTPError`. |
| `parseJson` | function | `JSON.parse` | Replaces JSON parsing for response bodies, including handling an empty body. |

## Pitfalls

- Do not use a TypeScript type parameter as a runtime check. Use `.json(schema)` when the response must be validated.
- `.json()` cannot parse an empty body by default. Configure `parseJson` when an endpoint legitimately returns an empty response.
- Read failed-response content from `HTTPError.data`, not from `HTTPError.response.json()`.
- Keep the request container's URL real in application code. The URLs above are examples; replace them with your API's endpoint.

## Related

- [Requests and responses](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/requests-and-responses)
- [Ky's error model](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/ky-error-model)
- [Error handling and validation](https://bench-ky-56.atloria.app/p/bench-ky-56-cVfxjVA4Qg/developer/error-handling-and-validation)
