colinhacks / zod

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

Support for date, time and date-time format strings #126

Closed maneetgoyal closed 1 year ago

maneetgoyal commented 4 years ago

Currently, Zod is lacking support for validating date, time and date-time stamps. As a result, users may need to implement custom validation via .refine. But the use case for date/time/date-time is very common, so if Zod ships it out-of-the-box like it does for uuid, email, etc, it would be helpful.

lazharichir commented 4 years ago

Agreed! Although date and time validation is a tricky issue as different developers expect different data (ISO? YYYY-MM-DD? 12h Time? With milliseconds? Timezone notation?).

maneetgoyal commented 4 years ago
maneetgoyal commented 4 years ago

If it helps, here's how djv is handling date-time strings. Their approach looks a bit similar to the idea that emerged in https://github.com/vriad/zod/issues/120 (i.e. using instantiation as a way to validate).

However, this approach may come with its own drawbacks.

grreeenn commented 4 years ago

I use the .refine() method for these cases, works like a charm and covers all of the special cases I need it to cover without introducing all of the datetime-standards related clutter

marlonbernardes commented 4 years ago

Hey @vriad are you still open to having AJV date/time/date-time regexes as built-ins? If so I'll open a PR sometime this week.

colinhacks commented 4 years ago

RFC for datetime methods

The hard problem here is naming the methods such that the validation behavior is clear to everyone. I'm opposed to adding a method called .datetime() since different people will have different ideas of what should be valid. I don't like AJV's datetime regex since it allows timezone offsets, which should almost never ever be used imo.

Here is a set of method names I'd be happy with:

The longer method name iso8601 makes the validation rule explicit. The documentation will clearly indicate that using UTC date strings is a best practice and will encourage usage of .utc() over .iso8601().

Milliseconds will be supported but not required in all cases.

eberens-nydig commented 4 years ago

Could something be achieved in user land via something akin to yup's addMethod?

It would be great to add in custom methods of built-in types to allow extensions.

colinhacks commented 4 years ago

I don't know how to implement addMethod such that TypeScript is aware of the new method. Since TypeScript isn't aware of the method there would (rightfully) be a SyntaxError if you ever tried to use it. This is an area where Yup is capable of more flexible behavior because it has less stringent standards for static typing.

eberens-nydig commented 4 years ago

I stumbled across this comment where you recommend a subclass. Given that could we achieve the same as above like so?

import { parse, parseISO } from 'date-fns';
import z from 'zod';

function dateFromString(value: string): Date {
  return parse(value, 'yyyy-MM-dd', new Date());
}

function dateTimeFromString(value: string): Date {
  return parseISO(value);
}

export class MyZodString extends z.ZodString {
  static create = (): ZodString =>
    new ZodString({
      t: ZodTypes.string,
      validation: {}
    });
  date = () => this.transform(z.date(), dateFromString);
  iso8601 = () => this.transform(z.date(), dateTimeFromString);
}

const stringType = ZodString.create;

export { stringType as string };

then consume...

import { string } from '.';
import z from 'zod';

export const schema = z.object({
  data: string().iso8601()
});
colinhacks commented 4 years ago

The general approach works but there are some problems with this implementation. You'd have to return an instance of MyStringClass from the static create factory. Here's a working implementation:

import * as z from '.';
import { parse, parseISO } from 'date-fns';

function dateFromString(value: string): Date {
  return parse(value, 'yyyy-MM-dd', new Date());
}

function dateTimeFromString(value: string): Date {
  return parseISO(value);
}

export class MyZodString extends z.ZodString {
  static create = () =>
    new MyZodString({
      t: z.ZodTypes.string,
      validation: {},
    });
  date = () => this.transform(z.date(), dateFromString);
  iso8601 = () => this.transform(z.date(), dateTimeFromString);
}

const stringType = MyZodString.create;
export { stringType as string };
eberens-nydig commented 4 years ago

Yes, thanks for the reply. Looks like I have some copypasta issues and didn't update the names. Thanks for confirming!

mmkal commented 3 years ago

@colinhacks what about using the built-in new Date(s), combined with an isNaN check, like io-ts-types does? That way if people want something other than the no-dependencies, vanilla JS solution, they can implement it themselves pretty easily with something like the above. The vast majority of people who just want something like this will be happy:


const User = z.object({
  name: z.string(),
  birthDate: z.date.fromString(),
})

User.parse({ name: 'Alice', birthDate: '1980-01-01' })
// -> ok, returns { name: 'Alice', birthDate: [[Date object]] }

User.parse({ name: 'Bob', birthDate: 'hello' })
// -> Error `'hello' could not be parsed into a valid Date`

User.parse({ name: 'Bob', birthDate: 123 })
// -> Error `123 is not a string`
Sytten commented 3 years ago

This should be implemented!

devinhalladay commented 3 years ago

Any updates on this?

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

fev4 commented 2 years ago

It'd be great to have this as proposed in the RFC. Or if we could just easily extend it with isDate, parse from date-fns or similar libraries.

helmturner commented 2 years ago

What if this were implemented

RFC for datetime methods

The hard problem here is naming the methods such that the validation behavior is clear to everyone. I'm opposed to adding a method called .datetime() since different people will have different ideas of what should be valid. I don't like AJV's datetime regex since it allows timezone offsets, which should almost never ever be used imo.

Here is a set of method names I'd be happy with:

  • z.string().date()2020-10-14
  • z.string().time()T18:45:12.123 (T is optional)
  • z.string().utc()2020-10-14T17:42:29Z (no offsets allowed)
  • z.string().iso8601()2020-10-14T17:42:29+00:00 (offset allowed)

The longer method name iso8601 makes the validation rule explicit. The documentation will clearly indicate that using UTC date strings is a best practice and will encourage usage of .utc() over .iso8601().

Milliseconds will be supported but not required in all cases.

I 100% agree with implementing validators that enforce best practices (i.e. iso8601 strings) for Date objects, but perhaps a more useful feature would be validators that conform to the TC39 proposal for the Temporal global.

I'd certainly feel more comfortable using the polyfill if zod provided a set of validators for the proposal!

samchungy commented 2 years ago

RFC for datetime methods

The hard problem here is naming the methods such that the validation behavior is clear to everyone. I'm opposed to adding a method called .datetime() since different people will have different ideas of what should be valid. I don't like AJV's datetime regex since it allows timezone offsets, which should almost never ever be used imo.

Here is a set of method names I'd be happy with:

  • z.string().date()2020-10-14
  • z.string().time()T18:45:12.123 (T is optional)
  • z.string().utc()2020-10-14T17:42:29Z (no offsets allowed)
  • z.string().iso8601()2020-10-14T17:42:29+00:00 (offset allowed)

The longer method name iso8601 makes the validation rule explicit. The documentation will clearly indicate that using UTC date strings is a best practice and will encourage usage of .utc() over .iso8601().

Milliseconds will be supported but not required in all cases.

:wave: I know this has been a long time since this has been discussed. I'm fairly happy to implement some of this but I think Milliseconds will be supported but not required in all cases. could be confusing? I feel like you would want users to supply either one or the other and not both for consistency sake? For that reason I often use the simple Date.toISOString() function to generate timestamps.

However, in terms of implementation if we wanted to enable both we could go with something like

z.string().utc(); // accept both milliseconds and no milliseconds
z.string().utc({ milliseconds: false }); // accept only no milliseconds
z.string().utc({ milliseconds: true }); // accept only milliseconds
colinhacks commented 1 year ago

Support for a configurable z.string().datetime() has landed in Zod 3.20. https://github.com/colinhacks/zod/releases/tag/v3.20

z.string().datetime()

A new method has been added to ZodString to validate ISO datetime strings. Thanks @samchungy!

z.string().datetime();

This method defaults to only allowing UTC datetimes (the ones that end in "Z"). No timezone offsets are allowed; arbitrary sub-second precision is supported.

const dt = z.string().datetime();
dt.parse("2020-01-01T00:00:00Z"); // 🟢
dt.parse("2020-01-01T00:00:00.123Z"); // 🟢
dt.parse("2020-01-01T00:00:00.123456Z"); // 🟢 (arbitrary precision)
dt.parse("2020-01-01T00:00:00+02:00"); // 🔴 (no offsets allowed)

Offsets can be supported with the offset parameter.

const a = z.string().datetime({ offset: true });
a.parse("2020-01-01T00:00:00+02:00"); // 🟢 offset allowed

You can additionally constrain the allowable precision. This specifies the number of digits that should follow the decimal point.

const b = z.string().datetime({ precision: 3 })
b.parse("2020-01-01T00:00:00.123Z"); // 🟢 precision of 3 decimal points
b.parse("2020-01-01T00:00:00Z"); // 🔴 invalid precision

There is still no .date() or .time() method, mostly because those use cases can be trivially implemented with .regex(). They may get added down the road, but I'm going to call this issue resolved.

iSeiryu commented 1 year ago

@colinhacks It looks like datetime accepts the offset as hh:mm only, but according to this https://en.wikipedia.org/wiki/UTC_offset hhmm should be accepted as well. Some frameworks generate it as hhmm by default and that should be an acceptable value.

is generally shown in the format ±[hh]:[mm], ±[hh][mm], or ±[hh]. So if the time being described is two hours ahead of UTC (such as in Kigali, Rwanda [approx. 30° E]), the UTC offset would be "+02:00", "+0200", or simply "+02".

samchungy commented 1 year ago

@colinhacks It looks like datetime accepts the offset as hh:mm only, but according to this https://en.wikipedia.org/wiki/UTC_offset hhmm should be accepted as well. Some frameworks generate it as hhmm by default and that should be an acceptable value.

is generally shown in the format ±[hh]:[mm], ±[hh][mm], or ±[hh]. So if the time being described is two hours ahead of UTC (such as in Kigali, Rwanda [approx. 30° E]), the UTC offset would be "+02:00", "+0200", or simply "+02".

I can speak to this but the initial regex was based on a StackOverFlow post which referenced the W3 spec for date time. It's also the default format for new Date().toISOString().

iSeiryu commented 1 year ago

ISO-8601 is actually hhmm: https://www.utctime.net/

image

asnov commented 1 year ago

This method defaults to only allowing UTC datetimes (the ones that end in "Z").

Does it mean that there are non-default options (for the ones that doesn't end in "Z")? How to enable them?

shoooe commented 1 year ago

There is still no .date() or .time() method, mostly because those use cases can be trivially implemented with .regex().

@colinhacks Can they though? The trivial implementation is probably along the lines of [0-9]{4}-[0-9]{2}-[0-9]{2} but this would also accept 2023-13-45 which is not a valid date. Similarly for time something like [0-9]{2}:[0-9]{2}:[0-9]{2} would accept times like 45:98 which are not valid.

samchungy commented 1 year ago

There is still no .date() or .time() method, mostly because those use cases can be trivially implemented with .regex().

@colinhacks Can they though? The trivial implementation is probably along the lines of [0-9]{4}-[0-9]{2}-[0-9]{2} but this would also accept 2023-13-45 which is not a valid date. Similarly for time something like [0-9]{2}:[0-9]{2}:[0-9]{2} would accept times like 45:98 which are not valid.

Feel like you cut the quote off short there. He does mention that it can be add down the road. Feel free to contribute if you'd like. You can probably use the existing datetime regex to figure something out though I don't think that's perfect either.

This method defaults to only allowing UTC datetimes (the ones that end in "Z").

Does it mean that there are non-default options (for the ones that doesn't end in "Z")? How to enable them?

It's literally in the section below where you just read?

ShivamJoker commented 1 year ago

z.string().date() I would want to have this in case I just want user to pass the date. I know it can be done via RegEx but it's not performant.

0xturner commented 1 year ago

This method defaults to only allowing UTC datetimes (the ones that end in "Z").

Does it mean that there are non-default options (for the ones that doesn't end in "Z")? How to enable them?

@asnov There is an open issue for supporting this #2385

And I've just opened a PR to support non-UTC time here #2913

mbsanchez01 commented 11 months ago

Having z.string().date() is necessary when using zod with other packages that generate the Open API spec, else the spec should say that the field is a string without format, when it should have the format $date

tonydattolo commented 10 months ago

@mbsanchez01 Similar concerns -- I'm using https://github.com/astahmer/typed-openapi for Zod object generation based on openapi spec. Is there a tool you're using to properly generate Zod date objects from openapi? Zod generator recognizes them as just strings.

Have an open issue here for generation of format: date Zod string objects: https://github.com/astahmer/typed-openapi/issues/24#issue-2064548824

mbsanchez01 commented 10 months ago

@mbsanchez01 Similar concerns -- I'm using https://github.com/astahmer/typed-openapi for Zod object generation based on openapi spec. Is there a tool you're using to properly generate Zod date objects from openapi? Zod generator recognizes them as just strings.

Hey @tonydattolo I'm using nestjs-zod which extends zod with a new method dateString

KUSHAD commented 7 months ago

date of type - 2024-03-14T20:00 is marked as invalid date-time, output like this when using <input type="datetime-local" />