zod icon indicating copy to clipboard operation
zod copied to clipboard

Add `.meta()` to zod types

Open iway1 opened this issue 2 years ago • 7 comments

Inspecting zod objects directly can be really useful for developing certain types of libraries. Of course tons of libs are already doing it - everything in the zod-to-x section of the documentation for example. Zod already supports some forms of meta data (custom error messages are a sort of metadata), which is extremely useful for things like building forms.

What if it were possible to attach arbitrary information, (or even just a string) via a meta function call? Having an explicit "meta" field could allow library developers to have their code react to zod schemas with much higher levels of customization, which could in turn allow us to improve developer experience in new ways.

The example I have in mind is this - Lets look at a tRPC procedure with a zod schema as an input validator:

getPosts: t.procedure.input(z.object({
  id: z.string().meta("The id of the post."),
  search: z.string().meta("Returns posts with this term in the body").
})).query(() => {
  //...
}),

Here we're attaching a description to each input parameter. Now we can do things like generating documentation directly from our validation schemas. Anyone who's developing a library that inspects zod schemas would have much more flexibility in the types of APIs they'd be able to develop.

It doesn't have to be a string, it could be any other typed object. One solution could be .meta<MetaType>(), or maybe an optional interface for generating a meta enabled client (probably a lot more difficult to create?) export const z = createZodClient<Meta>()

Selfishly, I'm developing a tool that inspects TRPC router and generates a manual testing / documentation UI automatically, which generates forms based on input zod schemas. With a meta function like this, it would allow developers to create documentation per input field with a minimal amount of effort. For me I'd just want a string but IDK if there are use cases for more complex objects or not

Would really love to add something like this, thoughts?

iway1 avatar Dec 17 '22 22:12 iway1

I was just checking out a lib called https://github.com/StefanTerdell/zod-to-json-schema and it seems they are converting json schemas's description with using .describe(). .describe() seems to be undocumented but already there. The drawback is that it only seems to support strings atm. It would indeed be very cool if we can define our own meta types.

This issue also seems to request this, but maybe opinions on this have shifted.

This seems to work currently:

const test = z.string().describe(({
   test: "123"
 } as unknown) as string);

 console.log(test.description);

So I wonder if zod actually does anything internally with description other then setting it.

That being said, I think it would be awesome if we have some examples on how to extend the zod core to add our own version of for example .meta<MetaType>() . Are there any hooks or middleware ways to make this happen without having to fork the project?

jrmyio avatar Dec 19 '22 09:12 jrmyio

Hi all,

This doesn't seem like a big lift. However, I wonder how prevalent is this use for integrating into other APIs? I certainly see the benefit (esp since other libraries are already weaponizing the .describe()), but I mean specifically the integration path: adding objects to a schema?

If we're going to work on making API integration accessible, I'd love to look at the ecosystem at large and see how other top-shelf libraries open themselves up to extensibility. Is it enough to add arbitrary objects to a schema? Or perhaps is there another introspection utility (maybe off of schema._def) that could be built and would be better suited long term?

maxArturo avatar Dec 19 '22 13:12 maxArturo

It supports .describe() to add a description

But you can add when are creating a schema too like that:

const zodNumber = z.number({
  description: 'My zod number',
})

For me I miss the ability to access if is required or not in the .transform() or in the .refine()

I think zod could fix that by creating a obj meta and add things like required, description to solve that

AndreiLucas123 avatar Dec 19 '22 19:12 AndreiLucas123

Yeah I understand the opinions in https://github.com/colinhacks/zod/issues/273. It could very easily lead to anti-patterns however I can also see the benefits of adding a meta field. At the moment I'm using https://github.com/asteasolutions/zod-to-openapi which extends Zod by binding to methods. Wrapping the zodobject eg meta(zodObject, { examples: 1}) feels clunky compared to zodobject.meta()

Would be very useful to be able to add more metadata officially which could be widely used by doco tools. eg. examples, description (I know description is already supported).

samchungy avatar Dec 21 '22 01:12 samchungy

I'm sure it could be used in pretty dumb ways, but potentially in pretty cool ways as well.

As someone who's built and worked on zod-introspecting packages and who uses zod schemas constantly (for form validation, automatic form construction, and trpc input validation), I can at least attest that having it being natively supported anywhere the schema._def as being very convenient during introspecting, and .meta() being probably the most natural or end-user-developers.

Personally, I'd prefer a way to have .meta to be able to be any object but also be typed upfront, to avoid having to explicitly type each call to .meta, that seems like a great DX and would support any extensions. (Having to do .meta<Type>() would be pretty tedious). I do really like the idea of just being able to create a custom zod instance that comes with a typed meta.

But even just some extra pre-defined meta fields like @samchungy is suggesting would be very useful on their own without opening the door to too much craziness.

iway1 avatar Dec 21 '22 01:12 iway1

Interesting, so zod has a meta like object which is _def

The problem is it loses information whenever the schema is wrapped (.optional(), .nullish(), etc...)

AndreiLucas123 avatar Dec 21 '22 16:12 AndreiLucas123

@AndreiLucas123 each successive .optional(), et al. call "wraps" the underlying schema in a chain of nested children. So, its all in there but you would have to recurse manually to get to any of it - or build an API around it proper.

In any case it seems @colinhacks came down on one side under #273 (and I can't say I really disagree). Less is more and all that. FWIW, I don't think I'd ever see any other name brand "utility" packages (think of io-ts, lodash, caolan's async etc) catering to downstream package plugins and so on - especially when wrapping them is very easy.

Although! There could be great value in documenting the available internals of zod so that other devs can easily introspect/extend the schemas.

maxArturo avatar Dec 21 '22 20:12 maxArturo

@maxArturo I think docs are a great idea here! I sure could've used them. I may write a short-but-potentially-useful guide up soon

Anyways - closing this, thanks for the discussion. Doesn't seem super clear whether meta() is a good idea or not

iway1 avatar Dec 23 '22 23:12 iway1

I wish to add some thoughts on this subject, should I do it in this issue, on #273 or create another one?

🤔

SkyaTura avatar Feb 09 '23 13:02 SkyaTura