colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
31.85k stars 1.11k forks source link

Feature Proposal: Functionalities for adding label and example(s) #2548

Open StephanMeijer opened 1 year ago

StephanMeijer commented 1 year ago

Feature Proposal: Functionalities for adding label and example(s)

Introduction

This proposal aims to introduce new functionalities to Zod, drawing inspiration from the capabilities of Joi. The proposed enhancements will allow users to assign labels as well as ability to set or add input examples. The primary objective of these enhancements is to augment Zod's capabilities, enabling the extraction of vital data necessary for tasks such as constructing OpenAPI specifications.

Detailed Description

1. Setting name [^1][^3]

The proposed feature will allow users to set names for their schemas. This will provide more context about the data and make it easier for developers to understand the purpose of each schema.

In style of .describe I propose the following:

.name

Use property name via the constructor to add a name property to the resulting schema.

const namedString = z
  .string({ name: "NamedString" });
documentedString.name; // NamedString

This can be useful for documenting a field, for example in a JSON Schema using a library like zod-to-json-schema).

This property is available in the error map when generating error messages.

2. Setting and Adding Example(s) [^2][^4]

In addition to setting descriptions and labels, the proposed feature will also allow users to set or add examples of input. This will provide a practical illustration of how the schema should be used, making it easier for developers to understand and implement.

In style of .describe I propose the following:

.exemplify

Use .exemplify() to add add an examples property to the resulting schema.

const exampleString = z
  .string()
  .exemplify("Some example");
documentedString.examples; // ['Some example']
const exampleString = z
  .string()
  .exemplify("Some example", "Another one");
documentedString.examples; // ['Some example', 'Another...

This can be useful for documenting a field, for example in a JSON Schema using a library like zod-to-json-schema).

Benefits

The foremost benefit of this proposal is the enhanced ability to extract data from Zod. This is crucial for tasks such as building OpenAPI specifications. By providing more context about the data (through descriptions and labels) and practical examples of use, developers will find it easier to understand and use the schemas. This, in turn, will lead to more efficient development processes and higher-quality output.

Conclusion

The proposed enhancements to Zod, inspired by Joi, will significantly improve the platform's functionality and usability. By allowing users to set labels and examples of input, we can make Zod more intuitive and effective for developers. This will ultimately lead to better data extraction capabilities, which is crucial for tasks such as building OpenAPI specifications.

Earlier proposals

Pull Requests made


Points of discussion

  1. Do we need a property for notes?[^5]
  2. Do we need a property for tags?[^6]
  3. Do we need a property for unit?[^7]

[^1]: For name we need other words, as the function cannot be the same as the property being set. [^2]: I am open for using another term. [^3]: Inspired by https://joi.dev/api/?v=17.9.1#anylabelname [^4]: Inspired by https://joi.dev/api/?v=17.9.1#anyexampleexample-options [^5]: Inspired by https://joi.dev/api/?v=17.9.1#anynotenotes [^6]: Inspired by https://joi.dev/api/?v=17.9.1#anytagtags [^7]: Inspired by https://joi.dev/api/?v=17.9.1#anyunitname

StephanMeijer commented 1 year ago

@StefanTerdell I am looking forward to your feedback!

StefanTerdell commented 1 year ago

By and large it LGTM. Some thoughts:

  1. IMHO label should be called title. label sounds to me like it could be a description or a title, and we already have describe.
  2. exemplify sounds a little odd to me, but then I'm not a native speaker. I guess based on [3] you're not 100% sure either though 😅
  3. I like the spread array syntax for the params in exemplify but is there precedence in the current Zod API somewhere? Can we come up with some reason not to use it? I guess these values would not be validated outside of their type, so an error message parameter isn't one of them
  4. Would the values in exemplify be typed against Input, infer | Output, either or none? I'm a bit biased towards Input as zod-to-json-schema reflects that (kind of) but typically in TS you'd be using Output. I'd like to see Array<Input | Output> but maybe this should even be flagged somehow, or be different functions, ie .examplifyInput / .examplifyOutput
  5. Regarding notes, tags and units: in my opinion label, description and examples should cover most cases and aligns with JSON schema as well.
StephanMeijer commented 1 year ago

@StefanTerdell Thank you for your review

By and large it LGTM. Some thoughts:

  1. IMHO label should be called title. label sounds to me like it could be a description or a title, and we already have describe.

Yeah but with what verb? Titleize? I'm really not sure how to follow naming conventions on this one.

  1. exemplify sounds a little odd to me, but then I'm not a native speaker. I guess based on [3] you're not 100% sure either though 😅

Yeah, couldn't come up with another verb except for addExample, setExamples, etc, but that doesn't really follow naming conventions.

  1. I like the spread array syntax for the params in exemplify but is there precedence in the current Zod API somewhere? Can we come up with some reason not to use it? I guess these values would not be validated outside of their type, so an error message parameter isn't one of them

My current PR does not implement it, but I think it's a good point of discussion.

  1. Would the values in exemplify be typed against Input, infer | Output, either or none? I'm a bit biased towards Input as zod-to-json-schema reflects that (kind of) but typically in TS you'd be using Output. I'd like to see Array<Input | Output> but maybe this should even be flagged somehow, or be different functions, ie .examplifyInput / .examplifyOutput

It would be typed against unknown, as that follows the type signature of .parse ^1 and .parseAsync ^2

  1. Regarding notes, tags and units: in my opinion label, description and examples should cover most cases and aligns with JSON schema as well.

I do agree.tags would not be compliant with the OpenAPI Specification ^3, therefore I don't see a use-case for this. Same goes for notes and units. The only use-case I can think of might be to add metadata for following API specifications like FHIR ^4, I feel confidence in @colinhacks to consider this as he writes on his blog to be working in the healthcare API sector. ^5

Svish commented 1 year ago

examplify sounds like a Harry Potter spell or something.

Since the input and output matters here, how just calling it .sampleInput and .sampleOutput?

Svish commented 1 year ago

Would be great if #2387 was included in this as well, if it isn't already. Especially having the label available in the error map when generating error messages, would make it a lot easier to generate proper accessibility compliant error messages. It would make it possible to replace e.g. too short with "Password" is too short or The "Password" needs to be longer.

Currently in our solution we've had to work around this by authoring all error messages in a way that they can always be appended to a label, and then it's stuck together by an error message component instead. But that's not very flexible, and the error messages sometimes become kind of awkward since it doesn't always make sense to have the label at the start. 😕

scotttrinh commented 1 year ago

Great proposal @StephanMeijer !

I think something we haven't discussed yet is whether this metadata should have a well-known structure vs. just allowing arbitrary Record<string, string>. Or maybe even Record<string | symbol, string> and have Zod and other libraries publish their own Symbols?

My hesitation to support having a well-known structure is that then Zod becomes the intermediary between libraries who want to attach metadata, the end users, and between different libraries who might have different goals.

Definitely not a hard blocker, since I know other libraries have chosen the well-known structure route, but I think it would be useful to at least discuss the pros and cons from the perspective of Zod itself, library authors, and end users.

StefanTerdell commented 1 year ago

@StephanMeijer - Yeah but with what verb? Titleize? I'm really not sure how to follow naming conventions on this one.

True 🤔 Same issue with label though

@Svish - Since the input and output matters here, how just calling it .sampleInput and .sampleOutput?

I like this syntax FWIW. Typing it to unknown seems a bit odd in this case and would invariably create stale examples.

@Svish - [...] would make it a lot easier to generate proper accessibility compliant error messages.

I think that idea has been considered and rejected already in #1767

@scotttrinh - I think something we haven't discussed yet is whether this metadata should have a well-known structure

Great point. As far as sample data goes I think it would be useful to have a typed interface based on input- and/or output types, since this could serve as inline documentation in the Zod schema code itself. It's a little harder to see the use-case for label/title, especially with the error message case being rejected. Serving the schema to a custom error message parser could be a future case for this but yeah.

However, one thing need not exclude the other. Collecting some well known keys such as the ones described here and having them typed but still allowing for additional keys containing whatever you want and collecting them into a meta key could be one approach. Something like this:

type ZodMeta<Input, Output> = {
  title?: string;
  description?: string;
  inputExamples?: Input[];
  outputExamples?: Output[];
  [key: string]: unknown;
};

The current describe and description props could simply point into this meta object as well for backwards compatibility.

Just a thought ofc

Svish commented 1 year ago

@Svish - [...] would make it a lot easier to generate proper accessibility compliant error messages.

I think that idea has been considered and rejected already in #1767

The idea considered and rejected in #1767 was the label itself, regardless of its use, but that's exactly what this issue, #2548, is adding, or am I misunderstanding something? I just wish for that label to be available in the error handler, as it would make accessibility much easier to do.

StephanMeijer commented 1 year ago

@Svish That would be indeed a proper usecase, Shall I add it to the proposal?

Svish commented 1 year ago

@Svish That would be indeed a proper usecase, Shall I add it to the proposal?

Please do 😊👍

StephanMeijer commented 1 year ago

However, one thing need not exclude the other. Collecting some well known keys such as the ones described here and having them typed but still allowing for additional keys containing whatever you want and collecting them into a meta key could be one approach. Something like this:

type ZodMeta<Input, Output> = {
  title?: string;
  description?: string;
  inputExamples?: Input[];
  outputExamples?: Output[];
  [key: string]: unknown;
};

@StefanTerdell I'd rather see an implementation where inputExamples and outputExamples can be bound to eachother.

StephanMeijer commented 1 year ago

I hereby request feedback from the following developers:

Username Project
@sachinraja https://github.com/sachinraja/zod-to-ts
@anatine https://github.com/anatine/zod-plugins
@DavidTimms https://github.com/DavidTimms/zod-fast-check
@kbkk https://github.com/kbkk/abitia
@turkerdev https://github.com/turkerdev/fastify-type-provider-zod
@asteasolutions https://github.com/asteasolutions/zod-to-openapi
@incetarik https://github.com/incetarik/nestjs-graphql-zod
@samchungy https://github.com/samchungy/zod-openapi
@fabien0102 https://github.com/fabien0102/ts-to-zod
@johngeorgewright https://github.com/johngeorgewright/runtyping
@rsinohara https://github.com/rsinohara/json-to-zod
@Code-Hex https://github.com/Code-Hex/graphql-codegen-typescript-validation-schema
@CarterGrimmeisen https://github.com/CarterGrimmeisen/zod-prisma
@Southclaws https://github.com/Southclaws/supervillain
@omar-dulaimi https://github.com/omar-dulaimi/prisma-zod-generator
@omar-dulaimi https://github.com/omar-dulaimi/prisma-trpc-generator
@chrishoermann https://github.com/chrishoermann/zod-prisma-types

I think the proposal can possibly affect your implementation in positive sense, therefore I would like to know if I can ask for your feedback and take it into account for this proposal.

samchungy commented 1 year ago

Would be nice to have both if we can't have .meta()

Southclaws commented 1 year ago

I was looking for something like this a while ago, my goal was to wire up a Zod schema as the source of truth for the validation and labels of a react hook form. Unfortunately there was no way to access the data I needed for this. Some form of metadata would be very useful for advanced use-cases like this.

I'm also a fan of codegen, and while I prefer to generate from a static schema (such as OpenAPI) generating TypeScript code is a bit of a minefield so with Zod in a project, it's nice to go the other way around (Zod -> code) and having additional metadata could help here.

To play devil's advocate however, this could be considered unnecessary bloat. There's already a way to get the name of a field, so why does a field need a second name defined? (namedString and NamedString in the first example)

foxt commented 9 months ago

+1. It'd be great to be able to, for example, on the server send through the Schema('s .shape) and then on the UI automatically build a form from that schema. Suggested keys and then custom keys sounds good.

Also, how would multiple meta be handled,

For example, would ```ts let zSecret = z.string().regex(/^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/i) .meta({ placeholder: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", sensitive: true }) let Foo = z.object({ secret: zSecret.meta({ label: "Secret", description: "Enter your secret", icon: "password" }) }) ``` be automatically merged by Zod, in something like: ```js { foo: { meta: { placeholder: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", sensitive: true, label: "Secret", description: "Enter your secret", icon: "password" } } ``` or kept seperate, in something like: ```js { foo: { meta: { label: "Secret", description: "Enter your secret", icon: "password" }, inner: { meta: { placeholder: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", sensitive: true, } } ```

The former would be easier to work with (and probably people's first expectation), but the latter is more flexible (though I doubt people would need that level of granularity)

bardouni commented 1 week ago

It's so odd to me why there is no .label() or alternative that works like Joi. How are my app users supposed to understand what is an Array ?

image
Southclaws commented 1 week ago

@bardouni

How are my app users supposed to understand what is an Array

For this use-case, you can set a custom error message as the last argument to most of the schema parts, so in this case you'd set .min(1, "Must contain at least one item") for a more user-friendly non-technical error message.

Svish commented 1 week ago

@bardouni

How are my app users supposed to understand what is an Array

For this use-case, you can set a custom error message as the last argument to most of the schema parts, so in this case you'd set .min(1, "Must contain at least one item") for a more user-friendly non-technical error message.

Using custom error messages is not really feasible. For consistent error messages, it's much better to use a global error map.

The challenge is that error messages, to be accessible, should be referencing the name of the field it belongs to, which is not possible with zod because there's no label and even if it was, it's not available in the error map when generating the error message.

In our app we've currently worked around it by writing all error messages in a way that it can be appended to the field label, and then in the error component it will display label + ' ' + message.

This kind of works, but isn't very flexible. Would be much better if there was a way to label schemas and a way to access that label in the error map when generating an error for that field.