zod icon indicating copy to clipboard operation
zod copied to clipboard

Add a label function as like yup

Open yatendra121 opened this issue 2 years ago • 18 comments

I have used YUP which provides a label for changing the value of key. like address:

yup.string().required().max(150).label('Address')

But Zod has not any simple function as like that.

yatendra121 avatar Dec 28 '22 07:12 yatendra121

Maybe you could use .describe()? It's not in the docs, but there's an open issue for that #1443

StefanTerdell avatar Dec 28 '22 14:12 StefanTerdell

https://github.com/jquense/yup#schemalabellabel-string-schema

Schema.label(label: string): Schema

Overrides the key name which is used in error messages.

https://github.com/jquense/yup#tuple

import { tuple, string, number, InferType } from 'yup';

let schema = tuple([
  string().label('name'),
  number().label('age').positive().integer(),
]);

await schema.validate(['James', 3]); // ['James', 3]

await schema.validate(['James', -24]); // => ValidationError: age must be a positive number

InferType<typeof schema> // [string, number] | undefined


I'm not sure what .describe() does, but I don't think it's the same.

I don't see age in the error

const schema = z.tuple( [
    z.string().describe( 'name' ),
    z.number().describe( 'age' ).positive().int(),
] )

const result = schema.safeParse( [ 'James', -24 ] )
!result.success && console.log( result.error.issues[ 0 ] )
// {
//     code: 'too_small',
//     minimum: 0,
//     type: 'number',
//     inclusive: false,
//     exact: false,
//     message: 'Number must be greater than 0',
//     path: [ 1 ]
// }

JacobWeisenburger avatar Dec 28 '22 16:12 JacobWeisenburger

Ah, no, that's my bad, I misunderstood the question. While .describe() would be analog to "description" in JSON schema I guess this would be more like "title" along with the added function of shaping the default error messages. Neat idea.

StefanTerdell avatar Jan 10 '23 08:01 StefanTerdell

@JacobWeisenburger do you mind assigning this to me i have created a first draft for this label as well

I have tried to add that label function in ZodType but it doesn't seem to be the best way since .label() has to be first in the chain z.string().label().min(2) - labels will work for min condition but not for invalid_type or required condition

I have made a draft where I add a label in RawCreateParams and then it gets available in this._def.label which can be passed down further addIssueToContext -> makeIssue -> defaultErrorMap labels can be used here

one problem tho label has to be passed everywhere in addIssueToContext

I am sorry if I did something wrong I hope you overlook it since it's my first commit in open source

current draft : https://github.com/RG7279805/zod/tree/label-in-raw-params will create a PR after confirmation

Rishabhg71 avatar Jan 17 '23 13:01 Rishabhg71

PRs are always welcome. No need to wait for confirmation.

JacobWeisenburger avatar Jan 17 '23 14:01 JacobWeisenburger

@JacobWeisenburger i need your opinion before i continue with this PR as i mentioned should the label be as a method to class or its ok as RawCreateParams ?

Rishabhg71 avatar Jan 17 '23 14:01 Rishabhg71

I don't know that I have enough experience to make a decision about this. Perhaps @colinhacks would be a better person to ask.

JacobWeisenburger avatar Jan 17 '23 14:01 JacobWeisenburger

@JacobWeisenburger any idea what would be the best way to approach him? he hasn't responded to my last issue as well on https://github.com/colinhacks/zod/issues/1784#issuecomment-1379492840

Rishabhg71 avatar Jan 17 '23 14:01 Rishabhg71

Perhaps discord? https://discord.gg/RcG33DQJdf

JacobWeisenburger avatar Jan 17 '23 14:01 JacobWeisenburger

@JacobWeisenburger i have Messaged him on discord but i don't think he is gonna respond Nonetheless i will wait

Rishabhg71 avatar Jan 17 '23 15:01 Rishabhg71

Sorry. I wish I had a better answer for you.

JacobWeisenburger avatar Jan 17 '23 16:01 JacobWeisenburger

I don't love this API - I think it makes the process of "computing" error messages even more inscrutable. What does it even mean to call this method on a ZodObject schema? Or a ZodEffects?

I don't mean to say that this idea has no value. But I think limiting labels to primitive schemas would be reasonable. Which means defining it in params instead of with .label method.

z.string({ label: "Name" })

Even then I'd like to see a proposal for how this information would be used in the defaultErrorMap before anyone starts on implementation.

Though it'll have to be a markedly superior experience to the current approach, which is to specify custom error messages at the time of declaration:

z.string({ required_error: "Name must be defined", invalid_type_error: "Name must be a string" })

Which can be made less verbose with a helper function that returns { required_error: string; invalid_type_error: string }. It's already pretty clean and straightforward IMO so I'm hesistant to introduce additional complexity here unless there's a dramatic DX improvement to be had.

colinhacks avatar Jan 17 '23 20:01 colinhacks

@colinhacks, as you asked i, have made a working draft https://github.com/colinhacks/zod/pull/1902 (i messed up linking PR to this issue)

z.object({
  first: z.string({ label: "First Name", }).min(3),
})

it's only working for z.object({ first: z.string({ label: "First Name", }).min(3), }) at the moment but you will get an idea of how this is gonna + I have also added labeled messages in "en.ts" (all the messages are not good)

In answering your question about DX if we add labeled errors I think it will be great. when I used Zod for the first time this was a single feature I thought must exist to make this library complete

Without Labeled error

z.object({
  first: z.string().min(3),
}).safeParse({ first: "AB" })
// error message
// {
//   "code": "too_small",
//   "minimum": 3,
//   "type": "string",
//   "inclusive": true,
//   "exact": false,
//   "message": "Invalid input",
//   "path": [
//     "first"
//   ]
// }

we cant send it to a client or show it to a user directly we have to add customized errors everywhere even though if its a really simple message like "First name is Required" since "Invalid input" cant be sent

z.object({
  first: z.string({ invalid_type_error: "first name should be of String", required_error: "First name is required" }).min(3, { message: "first name should be more than 3 char" }),
})

just to add really simple messages it becomes mandatory to include a "message" for every check

With Labeled Errors

if the label is passed all those messages can be removed and that error can be passed directly to the client/user even if they want to customize the message further they can do so


z.object({
  first: z.string({ label: "First Name" }).min(3),
}).safeParse({ first: "AB" })
// {
//   "code": "too_small",
//   "minimum": 3,
//   "type": "string",
//   "inclusive": true,
//   "exact": false,
//   "message": "First Name must be at least 3 character(s)",
//   "label": "First Name",
//   "path": [
//     "first"
//   ]
// }


if you think this feature is worth added do you mind assigning it to me

Rishabhg71 avatar Jan 18 '23 16:01 Rishabhg71

Which can be made less verbose with a helper function that returns { required_error: string; invalid_type_error: string }. It's already pretty clean and straightforward IMO so I'm hesistant to introduce additional complexity here unless there's a dramatic DX improvement to be had.

I agree that it is better to point out custom errors wherever possible. But this is very difficult in an internationalized application. In that vein, it is convenient to have a global error translation and the ability to specify key labels.

hgenru avatar Jan 24 '23 10:01 hgenru

@colinhacks @RG7279805 any updates on this proposal?

Currently I'm using a rather brittle workaround for the lack of this feature:

/**
 * Stop-gap until https://github.com/colinhacks/zod/pull/1902 is merged.
 * @param schema Schema to label
 * @param label Label to apply
 * @returns Schema with label
 */
export function LabelModel<TSchema extends z.ZodTypeAny>(
  label: string,
  schema: TSchema
) {
  return Object.assign(schema, { _label: label }) as TSchema;
}

/**
 * Get the inner zod type through zod arrays, objects, optional, etc.
 * @param schema Schema to traverse
 * @returns Inner zod type
 */
export function getSchemaInnerType<T extends z.ZodTypeAny>(
  schema: T
): z.ZodTypeAny {
  if (schema instanceof z.ZodArray) {
    return getSchemaInnerType(schema._def.type);
  }
  if (schema instanceof z.ZodBranded) {
    return getSchemaInnerType(schema._def.type);
  }
  if (schema instanceof z.ZodNullable) {
    return getSchemaInnerType(schema._def.innerType);
  }
  if (schema instanceof z.ZodOptional) {
    return getSchemaInnerType(schema._def.innerType);
  }
  return schema;
}

/**
 * Retrieve the label from the model.
 * @param schema Schema to retrieve label from
 */
export function getModelLabel<T extends z.ZodTypeAny>(schema: T): string {
  // Cast as untyped here to trawl for the metadata.
  const modelInfo = schema as any;

  // Retrieve Model Label, this works in z.array too.
  return (
    modelInfo._label ??
    (getSchemaInnerType(schema) as any)?._label ??
    "Unknown Model"
  );
}

This workaround has the limited scope of determining what model is failing to parse in a distributed system, however, information is lost if you chain additional modifiers onto the schema, so ideally, there was a better way to do it.

WilliamABradley avatar May 04 '23 23:05 WilliamABradley

@WilliamABradley i have made a draft PR but haven't got any response back from colinhack

Rishabhg71 avatar May 05 '23 00:05 Rishabhg71

Any updates on this one?

mleister97 avatar Nov 02 '23 11:11 mleister97

Really wish we could have this feature, or a method to insert metadata directly into the schema

darklight9811 avatar Oct 18 '24 16:10 darklight9811

Internationalization is the main concern: Image

florian2peaches avatar Aug 28 '25 13:08 florian2peaches