Skip to content
D
Documentation

JSON requests and responses

how-to
2 min readUpdated

Send JSON and read responses

Use ky 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, 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 and isHTTPError handling, see 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

OptionTypeDefaultWhat it does
jsonany value accepted by JSON.stringify()not setSerializes the value into the request body and sets Content-Type: application/json unless your headers override it.
headersFetch headersnot setSupplies request headers; an explicit Content-Type takes precedence over Ky's JSON header.
throwHttpErrorsbooleanenabledControls whether non-2xx responses reject as HTTPError.
parseJsonfunctionJSON.parseReplaces 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.

Was this page helpful?