zod
zod copied to clipboard
Feature: Add `z.jsonString` schema primitive
Hi there! Great work with zod and v4! Now that it's easier to tree-shake zod thanks to v4-mini, would you be open to add more commonly-used schemas as primitives? Specifically, I'm thinking about json:
// Json types from https://github.com/sindresorhus/type-fest/blob/041e67e1f180b1d6adddca57f9429789c16edf81/source/json-value.d.ts#L31 inlined here to avoid adding a dependency
export type JsonObject = {[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined};
export type JsonArray = JsonValue[] | readonly JsonValue[];
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
export function json() {
return z.string().transform((str, ctx): JsonValue => {
try {
return JSON.parse(str) as JsonValue;
} catch {
ctx.addIssue({ code: "custom", message: "Invalid JSON", input: str });
return z.NEVER;
}
});
}
This is such a common use case that it would be awesome to have it as part of the library.
If you're in principle open to adding this, but don't have the time due to the current changes coming with v4, I'm also very happy to provide a PR.
Hey there, you can fine an existing JSON utility defined here https://zod.dev/api#json hope it's what you're after!
Thanks! Not sure how I overlooked that. But it looks like this expects an object as input, while the type I'm suggesting is expecting a string as input and is then JSON.parsing it.
Maybe adding coerce.json() would be the solution then?
I don't think there is currently a json parser for strings, but I'm sure you could probably wrap the native JSON.parse method to achieve what you wanted, if so, you would probably not need a specific json parser anyway. Good luck
Yes, JSON.parse works, see the code example above. This issue is just about making a common util available directly in zod.
While it seems reasonable that Zod could have a z.json that automatically calls JSON.parse() during Zod's own parse() call, ultimately
- This ship has sailed somewhat since
z.json()already expects a JSON-serializable object (this was probably an oversight in retrospect) - This is achievable with transforms/pipe as others have said.
If something like your proposal was to be added it would probably be called z.jsonString() and optionally accept a post-parse schema: z.jsonString(z.object({ ... })) but I'll need to ponder whether that's worthwhile at this point.
What about the coerce.json option? That feels like a natural interface and wouldn't be a breaking change.
I don't think that aligns with the Zod's concept of "coercion" - accepting unknown input and using JS built-ins to convert to a primitive value.
In the end it obviously is your call, and also jsonString could be a valuable addition - but JSON.parse is JS built-in, and the part of coerce.string that screams "primitive" is the .string, not the coerce, so coerce.json does feel to me like it fully aligns with what I would expect from coerce :)
I wouldn't blink an eyelid at coerce simple calling JSON.parse internally and propagating the error zod-like. Then passing the output to the actual z.json() schema.
I don't think that aligns with the Zod's concept of "coercion" - accepting unknown input and using JS built-ins to convert to a primitive value.
You guys are cherrypicking a bit. Your proposal is for a schema type that accepts a string (not unknown) and performs a well-defined JSON.parse operation (not a coercion).
Let's maybe split into two questions then:
- is this parsing something that happens often enough so that it warrants being added to zod directly?
- if yes, what's the best way to add it
If we agree on 1, then we still have other options for 2 like jsonString in case coerce feels wrong to you.
On the surface, it feels useful, but in reality I find most of the time other frameworks are doing the parsing of stringified JSON for me. db clients, http libraries, etc, mostly expose the object already transformed by the time I see it.
There are some cases I can think of where databases that don't natively support json types need "help" marshalling and unmarshalling these types, and in those cases, a zod integration would be useful. my current prod forms save arbitrarily structured json blobs representing draft form-data in mssql, where it currently stores as a string.
Most cases I think this benefits from is library maintainers, not library users.
Some context/solutions in a previous request:
- https://github.com/colinhacks/zod/discussions/2215
I also was expecting z.json to accept "JavaScript Object Notation", not "objects that can be turned into a JavaScript Object Notation string". This is like having z.html() accept elements (z.html().parse(document.body)). The method should have been called z.jsonifiable
The documentation is really lacking in this regard.
we have similar helper in our codebase. A generic based on transform and with ability use zod to validate the contents of the json
export const json = <T extends z.ZodTypeAny>(
type: T,
): z.ZodPipe<z.ZodTransform<unknown, unknown>, T> =>
z.preprocess((input, ctx) => {
if (typeof input !== "string") return input
try {
return JSON.parse(input)
} catch {
ctx.addIssue({ code: "custom", message: "Invalid JSON", input })
return z.NEVER
}
}, type)
// usage
const schema = json(z.object(hello: z.string()))
schema.parse({hello: "world"}) //pass
schema.parse('{hello: "world"}') //pass
schema.parse("not a json") // fail - invalid json
schema.parse("{}") // fail - missing field hello
Hi @colinhacks, I would like to tackle this issue by adding z.jsonString() as you mentioned above.
Problem:
Zod already has strong support for JSON-like objects (z.json()), but there’s a common gap:
often data is received as stringified JSON (e.g. from form fields, DB blobs, query params).
Right now, the pattern is always two steps:
let parsed;
try {
parsed = JSON.parse(input);
} catch {
throw new Error("Invalid JSON");
}
const data = mySchema.parse(parsed);
This works, but introduces friction:
- Error handling inconsistency → invalid JSON throws native
SyntaxErrorinstead of aZodError. - Boilerplate everywhere → every codebase reimplements the same
try/catch + JSON.parsewrapper. - Weaker composability → you can’t simply
.pipe()or.transform()from a JSON string input in a single Zod schema.
Proposal:
Introduce z.jsonString(schema?: ZodSchema) which:
- Accepts a string.
- Attempts JSON.parse.
- On failure, adds a proper Zod issue (Invalid JSON).
- On success, runs the parsed value through the optional provided schema.
Example:
const userSchema = z.jsonString(
z.object({ name: z.string(), age: z.number() })
);
userSchema.parse('{"name":"Sky","age":23}');
// ✅ { name: "Sky", age: 23 }
userSchema.parse("not json");
// ❌ ZodError: Invalid JSON
Any specific guidance before i start?