zod icon indicating copy to clipboard operation
zod copied to clipboard

Add z.string().json(...) helper

Open mmkal opened this issue 1 year ago • 1 comments

Based on https://github.com/colinhacks/zod/issues/3077#issuecomment-1878470503. I started playing around to see how quick it would be to implement, and it ended up being very quick - so opening a PR in hopes the idea is accepted.

Copying from the readme for the z.string().json(...) method - the linked issue has more details on the motivation:

JSON

The z.string().json(...) method parses strings as JSON, then pipes the result to another specified schema.

const Env = z.object({
  API_CONFIG: z.string().json(
    z.object({
      host: z.string(),
      port: z.number().min(1000).max(2000),
    })
  ),
  SOME_OTHER_VALUE: z.string(),
});

const env = Env.parse({
  API_CONFIG: '{ "host": "example.com", "port": 1234 }',
  SOME_OTHER_VALUE: "abc123",
});

env.API_CONFIG.host; // returns parsed value

If invalid JSON is encountered, the syntax error will be wrapped and put into a parse error:

const env = Env.safeParse({
  API_CONFIG: "not valid json!",
  SOME_OTHER_VALUE: "abc123",
});

if (!env.success) {
  console.log(env.error); // ... Unexpected token n in JSON at position 0 ...
}

This is recommended over using z.string().transform(s => JSON.parse(s)), since that will not catch parse errors, even when using .safeParse.

mmkal avatar Jan 05 '24 11:01 mmkal

Deploy Preview for guileless-rolypoly-866f8a ready!

Built without sensitive environment variables

Name Link
Latest commit 3a0cdb4cb8a100bb0d8a2b4442b82bbf118e2f8d
Latest deploy log https://app.netlify.com/sites/guileless-rolypoly-866f8a/deploys/65a1b034f44f690008a4716c
Deploy Preview https://deploy-preview-3109--guileless-rolypoly-866f8a.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

netlify[bot] avatar Jan 05 '24 11:01 netlify[bot]

@colinhacks any thoughts on this?

mmkal avatar Mar 19 '24 21:03 mmkal

Thanks, great stuff. I'm merging this into the v4 branch as a starting point, but the API is likely to change before this lands. I actually think this would be better as a top-level z.json(), and z.string().json() should be reserved for verifying that the string is in fact a valid JSON string.

colinhacks avatar Apr 29 '24 22:04 colinhacks

@colinhacks what would be the feature of a z.json? Parsing and stringify?

m10rten avatar Apr 30 '24 10:04 m10rten

Yes, a schema that encapsulates the JSON.parse step. Input is string, Output is inferred from the arguments:

const schema = z.json(z.object({ name: z.string() }))
schema.parse(`{ "name": "Maarten" }`)

colinhacks avatar May 02 '24 20:05 colinhacks

That looks clean, however would that not be this: (?)

const jsonStr = `{ "name": "Jhon Doe" }`;
const schema = z.object({
  name: z.string(),
});
const anyJsonObj: unknown = JSON.parse(jsonStr);
const parsed = schema.parse(anyJsonObj);
// ^? type: z.infer<typeof schema>

I could be wrong, but the .json would be a replacement for JSON.parse, where this function: MDN Docs: JSON.parse also takes in a reviver, how are you planning to resolve that since any object can be different, users can make their own revivers and set custom properties to, for example, make a map or set in JSON.

So by making this 'replacement' as to call it, would you not need way more steps to have the same, generic, result?

m10rten avatar May 03 '24 08:05 m10rten

Was just going to comment re: this, it would be good to have a method like

const Config = z.json(
  z.object({
    apiKey: z.string(),
    templateId: z.string(),
  })
).reviver((k, v) => typeof v === 'number' ? v.toString() : v)

const result = Config.parse(process.env.CONFIG) // '{"apiKey":"x", "templateId":123}' => {apiKey: 'x', templateId: '123'}

You could often achieve a similar result with .transform or .preprocess but there are still plenty of valid use cases for reviver.

But @m10rten it's not really the same as calling JSON.parse(jsonStr) - that will throw an error directly. It won't be encapsulated by safeParse, won't be wrapped as a zod error. And it becomes much more inconvenient to compose, e.g. if it's in a nested property like:

const BigSchema = z.object({
  type: z.string(),
  foo: z.number(),
  env: z.object({
    NODE_ENV: z.string(),
    CONFIG: z.json(z.object({apiKey: z.string()})).reviver(...)
  }),
})

mmkal avatar May 03 '24 13:05 mmkal