Closed maneetgoyal closed 1 year 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?).
zod.string().regex(...)
is available since v1.11
, it has become easier to implement custom date/time/date-time patterns. zod.string().regex()
, users can modify the default behavior if it doesn't meet their expectations. 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.
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
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.
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.
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.
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.
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()
});
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 };
Yes, thanks for the reply. Looks like I have some copypasta issues and didn't update the names. Thanks for confirming!
@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`
This should be implemented!
Any updates on this?
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.
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.
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!
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
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.
@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".
@colinhacks It looks like
datetime
accepts the offset ashh:mm
only, but according to this https://en.wikipedia.org/wiki/UTC_offsethhmm
should be accepted as well. Some frameworks generate it ashhmm
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()
.
ISO-8601 is actually hhmm
: https://www.utctime.net/
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?
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.
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 accept2023-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 like45: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?
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.
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
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
@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 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
date of type - 2024-03-14T20:00
is marked as invalid date-time, output like this when using <input type="datetime-local" />
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 foruuid
,email
, etc, it would be helpful.