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

Similar to Yup’s meta properties? #76

Closed
cybervaldez opened this issue Jun 25, 2020 · 10 comments
Closed

Similar to Yup’s meta properties? #76

cybervaldez opened this issue Jun 25, 2020 · 10 comments

Comments

@cybervaldez
Copy link

This is an extremely useful feature yup had that is keeping me from transitioning to zod, basically each schema object have additional properties that can be used for validation and other use case.

@colinhacks
Copy link
Owner

I'm considering this but I'm not sure I understand the use case. Can you elaborate on how exactly you hope to use this? Error customization? Custom validations?

@cybervaldez
Copy link
Author

cybervaldez commented Jul 1, 2020

primarily using it as part of refine's validation is one:

const myString = z.string().meta({ length : 255 }).refine( (val, meta) => val.length <= meta.length, ({length})=>"String can't be more than ${length} characters");

but equally important if zod would implement something like yup's describe:
const inputProps = myString.describe() // { type : 'string', meta : { length : 255 } }

so in our UI we can easily build our input based on that.
<input ... maxlength={inputProps.meta.length} />

@chrbala
Copy link
Contributor

chrbala commented Jul 18, 2020

Seems like there are quite of lot of arbitrary places where UI could be driven from metadata in some way:

  • max length on a field gets set on the input
  • max length on an array disables or hides an "add row" button

More complex metadata could be something like data driven UI allowing for selection of various complex forms based on a union type.

At some level it seems like there could be a question of whether zod:

  • allows arbitrary fields like this suggestion
  • generates user-accessible metadata based on the built-ins like z.string().max(5)
  • some combination of the above

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Aug 3, 2020

I use Yup's meta to add human-readable validation rules that are not used directly but can be displayed in forms.
Very useful feature. In this way you don't have to support a second tree of objects which is more ergonomic – rules are declared along with their descriptions (not scattered around):

  summary: Y.string().required()
    .min(3).max(500)
    .meta({tooltip: "3-500 chars"}),

--- vs ---

let makeSchema = () => {
  // validation tree
  return ...
    summary: Y.string().required()
      .min(3).max(500),
}

let makeTooltips = () => {
  // has to mirror the above
  return ...
    summary: {tooltip: "3-500 chars"},
}

@colinhacks
Copy link
Owner

colinhacks commented Aug 13, 2020

I'm afraid I'm getting more and more opposed to this over time. All of these use cases are out of scope for Zod and should be implemented with programmatic constructs external to Zod.

Form UI

@ivan-kleshnin

type MyFormInput = {
  name: string;
  label: string;
  placeholder: string;
  validator: z.ZodSchema<unknown>;
};
type MyForm = MyFormInput[];

const fields:MyForm = [ /* define form here */ ];

Then you can loop over the form fields to build your form. To get the type of each ZodSchema you can use input.validator._def.t:

const input: MyFormInput = "whatever" as any;
input.validator._def.t; // => z.ZodTypes

If you want to get really fancy and still get type inference, use a class. Here's a toy example of a ZodForm class. This could easily be a library unto itself.

class ZodForm<T extends MyFormInput[] = []> {
  inputs: T;
  constructor(inputs: T) {
    this.inputs = inputs;
  }

  input = <Input extends MyFormInput>(input: Input): ZodForm<[...T, Input]> => {
    return new ZodForm([...this.inputs, input]);
  };
}

For metadata driven validations

@cybervaldez

You should use language-level constructs for this too.

const maxRefinement = (max: number) => ({
  check: (val: string) => val.length <= max,
  message: `String can't be more than ${max} characters`,
})

const myString = z.string().refinement(maxRefinement(12));

More generally, I think of Zod as a foundation for typesafety in an application, designed to be built on top of. The use cases you gave are certainly valid, though they're the sort of domain-specific/application-specific tooling you should build yourself on top of Zod. Zod shouldn't be a wrapper for it, it should be a component within it. 👍

@ivan-kleshnin
Copy link
Contributor

ivan-kleshnin commented Aug 13, 2020

I kinda agree. This is solvable in user-land so maybe it's really not worth adding at the moment (if ever).
More code = More tests = Slower development.

@colinhacks
Copy link
Owner

given the lack of dissent, i'm gonna close this

@jedwards1211
Copy link

jedwards1211 commented Jan 11, 2024

There are problems I've only been able to solve by attaching metadata at multiple levels of the Zod schema and deeply inspecting the Zod schema at type time/runtime.

Wrapping a Zod schema in something else to attach metadata isn't deeply composable. What if you need to do something special on an array schema if the element schema has certain metadata attached to it? Well, now you have to make your own array wrapper type. And then you have to make your own set wrapper type, object wrapper type, etc... essentially you end up completely reimplementing the whole Zod hierarchy.

So, I was determined to find a way to keep the metadata within my Zod hierarchy. After a lot of experimentation, I landed on extending a no-op refinement ZodEffects as a way to attach a metadata property:

import z from 'zod'

export class ZodMetadata<
  T extends z.ZodTypeAny,
  M extends object
> extends z.ZodEffects<T> {
  constructor(def: ZodEffectsDef<T>, public metadata: M) {
    super(def)
  }

  unwrap() {
    return this._def.schema
  }
}

export function zodMetadata<T extends z.ZodTypeAny, M extends object>(
  schema: T,
  metadata: M
): ZodMetadata<T, M> {
  return new ZodMetadata(schema.refine(() => true)._def, metadata)
}

With this, you can do any deep mapping or filtering on the Zod schema you want. And not only can you inspect the metadata at runtime; you can even operate on it at type time!

type OutputForVersion<
  Z extends z.ZodTypeAny,
  V extends number
> =
  S extends ZodMetadata<infer T, infer M>
  ? M extends { version: infer SchemaVersion extends number }
    ? IsGreaterThanOrEqual<SchemaVersion, V> extends true
      ? OutputForVersion<T, V>
      : never
    : OutputForVersion<T, V>
  : IsAny<z.output<S>> extends true
  ? any
  : S extends z.ZodArray<infer T>
  ? OutputForVersion<T, V> extends never
    ? never
    : Array<OutputForVersion<T, V>>
  : S extends z.ZodUnion<infer T>
  ? OutputForVersion<T[number], V>
  : S extends z.ZodOptional<infer T>
  ? OutputForSingleVersion<T, V> extends never
    ? never
    : OutputForSingleVersion<T, V> | undefined
  // etc
  : z.output<S>

@vadimyen
Copy link

vadimyen commented May 3, 2024

@jedwards1211, how do you use the OutputForVersion type?

@jedwards1211
Copy link

jedwards1211 commented May 4, 2024

@vadimyen here's my blog post about the whole system: https://www.jcore.io/articles/schema-versioning-with-zod

At the bottom of the "Normalizing to the latest version with helper functions" section it shows how I'm indirectly using the OutputForVersion type

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

6 participants