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 Support for z.input / z.output #4

Closed
FlorianWendelborn opened this issue Sep 15, 2021 · 8 comments
Closed

Add Support for z.input / z.output #4

FlorianWendelborn opened this issue Sep 15, 2021 · 8 comments

Comments

@FlorianWendelborn
Copy link

Currently, this package seems to only be able to generate z.input. There’s differences between the two when using .default() for example.

@StefanTerdell
Copy link
Owner

Hello! Care to elaborate with an example?

@FlorianWendelborn
Copy link
Author

@StefanTerdell sure, here’s the documentation section for z.input/z.output: https://github.com/colinhacks/zod#what-about-transforms

While the example is an extreme case where it switches from string to number, a more common one would be using .default which switches from T | undefined to just T.

So, inferring z.input would yield T | undefined while the output (post-parsing) is just T.

const usingDefault = z.string().default('example')
type zInput = z.input<typeof usingDefault> // should be string | undefined
type zOutput = z.output<typeof usingDefault> // should be string (z.infer is an alias for z.output)

@StefanTerdell
Copy link
Owner

Thanks!

I may have been eating crayons again, but I don't see how this is relevant when converting to a Json Schema as the output schema would depend on the implementation of the Json Schema parser.

For instance, Ajv will quietly ignore defaults unless the useDefaults flag is passed as true and will leave the property undefined if not. Like so:

const mySchema = z.object({
    myString: z.string().default("hello"),
  });

const jsonSchema = zodToJsonSchema(mySchema);

const ajvWithDefaults = new Ajv({ useDefaults: true });
const dataWithDefaults = {}
ajvWithDefaults.validate(jsonSchema, dataWithDefaults)
console.log(dataWithDefaults)
// outputs: { myString: "hello" }

const ajvWithoutDefaults = new Ajv();
const dataWithoutDefaults = {}
ajvWithoutDefaults.validate(jsonSchema, dataWithoutDefaults)
console.log(dataWithoutDefaults)
// outputs: { }

Zod marks fields within an object with defaults as optional both in input and output and only "secretly guarantees" a value in the end. Like so:

const mySchema = z.object({
  myString: z.string().default("hello"),
});

type input = z.input<typeof mySchema>;
// Compiles to:
// type input = {
//    myString?: string;
// }
type output = z.output<typeof mySchema>;
// Also compiles to:
// type output = {
//     myString?: string;
// }

const data = mySchema.parse({})
// data = {
//   myString: "hello"
// }

The example you're using is also a bit broken since both types are inferred to simply string. In zod^3.x.x, optional fields aren't unions with undefined anymore under the hood.

But I'm getting the feeling that I'm missing something here.. Would be open to a clearer issue or a PR of course, but I think I'll go ahead and close this issue in the meantime.

Thanks for reaching out!

@FlorianWendelborn
Copy link
Author

Regarding your example, I think this may be different in the way you tested it. Perhaps a different TS version or no strict mode?

Here’s a repro for yielding two different input/output types from your example: TypeScript Playground

As you can see, the tooltip shows it as non-optional, as it should:
image

Here is the repro of my example, where you can also see it’s string | undefined vs string respectively:

TypeScript Playground

@FlorianWendelborn
Copy link
Author

I don't see how this is relevant when converting to a Json Schema

As far as I’m concerned, at least for my particular use-cases so far, JSON Schemas are just an intermediary step to convert X -> JSON -> TypeScript Interfaces with X being OpenAPI or zod.

And I’d be surprised if the JSON schema for an optional property looks exactly the same as the JSON schema for a non-optional property. I don’t really want to use JSON schemas in the first place, but they’re a pretty widely-supported intermediary between different typed formats, so maybe my assumptions are somehow wrong. That’s also why I can’t provide the expected generated types, as I’ve never used JSON schemas for anything but converting it to something else.

Also, there’s still the other edge-case with transforms where the input type could be string while the output is number. That certainly has to be reflected in the schema (although I personally only care about the default/partial/undefined support as I don’t use other forms of transforms)

@StefanTerdell
Copy link
Owner

Would you look at that! Looks like I have some form of environmental issue.

Anyway, I took a look at it and it seems like z.output just uses the inferred return type from the transform function, so there's no actual runtime schema for the output. In other words: it can't be done with the current Zod implementation. Sorry :(

Maybe running https://www.npmjs.com/package/ts-json-schema-generator or something like it on the inferred type could work but I haven't given it a try.

@StefanTerdell
Copy link
Owner

What I would need is something like transformInto(nextSchema, func) that would pass the new schema down the chain. There's a PR here for something similiar, but looks like it's gone stale: colinhacks/zod#420

@FlorianWendelborn
Copy link
Author

Then I think this should just be documented that this package handles z.input and not z.output aka z.infer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants