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

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

Closed
maneetgoyal opened this issue Aug 25, 2020 · 31 comments
Closed

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

maneetgoyal opened this issue Aug 25, 2020 · 31 comments

Comments

@maneetgoyal
Copy link

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
Copy link

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
Copy link
Author

  • Now that zod.string().regex(...) is available since v1.11, it has become easier to implement custom date/time/date-time patterns.
  • Some out-of-the-box support for date/time/etc. can still be useful though. Using zod.string().regex(), users can modify the default behavior if it doesn't meet their expectations.
  • The default behavior I was proposing is from AJV which is a hugely popular data validation library. I am assuming they'd have brought in many improvements to their regex over time making it increasingly robust. But yes, there can be some unhandled cases as suggested above in Support for date, time and date-time format strings #126 (comment) and in Date type handling? #30 (comment).

@maneetgoyal
Copy link
Author

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

However, this approach may come with its own drawbacks.

@grreeenn
Copy link

grreeenn commented Sep 20, 2020

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
Copy link
Contributor

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
Copy link
Owner

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.

@eberens-nydig
Copy link

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
Copy link
Owner

colinhacks commented Nov 11, 2020

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
Copy link

eberens-nydig commented Nov 11, 2020

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
Copy link
Owner

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
Copy link

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

@mmkal
Copy link
Contributor

mmkal commented Jan 28, 2021

@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
Copy link

Sytten commented Sep 20, 2021

This should be implemented!

@devinhalladay
Copy link

Any updates on this?

@stale
Copy link

stale bot commented Mar 2, 2022

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.

@stale stale bot added the wontfix This will not be worked on label Mar 2, 2022
@stale stale bot closed this as completed Mar 9, 2022
@fev4
Copy link

fev4 commented May 16, 2022

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
Copy link

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
Copy link
Contributor

samchungy commented Oct 13, 2022

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 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 colinhacks reopened this Nov 14, 2022
carlgieringer added a commit to carlgieringer/zod that referenced this issue Nov 19, 2022
@colinhacks colinhacks removed the wontfix This will not be worked on label Dec 13, 2022
@colinhacks
Copy link
Owner

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
Copy link

iSeiryu commented Feb 6, 2023

@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
Copy link
Contributor

@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
Copy link

iSeiryu commented Feb 8, 2023

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

image

@asnov
Copy link

asnov commented Feb 23, 2023

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
Copy link

shoooe commented Apr 1, 2023

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
Copy link
Contributor

samchungy commented Apr 2, 2023

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
Copy link

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
Copy link

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
Copy link

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
Copy link

tonydattolo commented Jan 3, 2024

@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: astahmer/typed-openapi#24 (comment)

@mbsanchez01
Copy link

mbsanchez01 commented Jan 4, 2024

@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
Copy link

KUSHAD commented Mar 25, 2024

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

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