Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Apr 29, 2024
Merged

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

merged 4 commits into from
Apr 29, 2024

Conversation

mmkal
Copy link
Contributor

@mmkal mmkal commented Jan 5, 2024

Based on #3077 (comment). 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.

Copy link

netlify bot commented Jan 5, 2024

Deploy Preview for guileless-rolypoly-866f8a ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 3a0cdb4
🔍 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.

src/types.ts Outdated
ctx.addIssue({
code: ZodIssueCode.invalid_string,
validation: "json",
message: (error as Error).message,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a quick scan, can't find anywhere else where error as Error is used.
So if you need it to be a message: string, check for instanceof first, or use the ...errorUtil.errToObj

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO as is better here than doing something at runtime. JSON.parse(...) only throws Errors (specifically SyntaxErrors) as documented by MDN. I also don't feel particularly strongly though.

@mmkal
Copy link
Contributor Author

mmkal commented Mar 19, 2024

@colinhacks any thoughts on this?

@colinhacks colinhacks changed the base branch from master to v4 April 29, 2024 20:30
@colinhacks colinhacks merged commit d7df683 into colinhacks:v4 Apr 29, 2024
@colinhacks
Copy link
Owner

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.

@m10rten
Copy link
Contributor

m10rten commented Apr 30, 2024

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

@colinhacks
Copy link
Owner

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 added a commit to ytsunekawa/zod that referenced this pull request May 3, 2024
* Add z.string().json(...) helper

* use parseAsync + await expect(...).rejects

* make it work in deno

* Add overload

---------

Co-authored-by: Misha Kaletsky <mmkal@users.noreply.github.com>
Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
@m10rten
Copy link
Contributor

m10rten commented May 3, 2024

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?

@mmkal
Copy link
Contributor Author

mmkal commented May 3, 2024

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 mmkal deleted the json branch September 18, 2024 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants