Skip to content

Commit 8671a0d

Browse files
authored
fix: time format should not have date type (#2471)
1 parent a394ddf commit 8671a0d

File tree

8 files changed

+137
-13
lines changed

8 files changed

+137
-13
lines changed

docs/languages/TypeScript.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ There are special use-cases that each language supports; this document pertains
77
<!-- toc -->
88

99
- [Generate an interface instead of classes](#generate-an-interface-instead-of-classes)
10+
- [Date and time format mappings](#date-and-time-format-mappings)
1011
- [Generate different `mapType`s for an `object`](#generate-different-maptypes-for-an-object)
1112
- [Generate union types instead of enums](#generate-union-types-instead-of-enums)
1213
- [Generate serializer and deserializer functionality](#generate-serializer-and-deserializer-functionality)
@@ -28,6 +29,27 @@ Sometimes you don't care about classes, but rather have interfaces generated. Th
2829

2930
Check out this [example out for a live demonstration](../../examples/typescript-interface).
3031

32+
## Date and time format mappings
33+
34+
JSON Schema string properties can specify a `format` keyword to indicate the expected data format. TypeScript maps these formats to appropriate types:
35+
36+
| JSON Schema format | TypeScript type | Notes |
37+
|--------------------|-----------------|-------|
38+
| `date-time` | `Date` | ISO 8601 date-time strings (e.g., `"2023-01-01T10:00:00Z"`) |
39+
| `date` | `Date` | ISO 8601 date strings (e.g., `"2023-01-01"`) |
40+
| `time` | `string` | Time-only strings (e.g., `"14:30:00"`) |
41+
| Other formats | `string` | Email, URI, UUID, etc. |
42+
43+
**Why is `time` mapped to `string`?**
44+
45+
Time-only strings like `"14:30:00"` are not valid JavaScript `Date` constructor arguments. Passing them to `new Date()` produces unreliable results (implementation-dependent behavior). Therefore, `format: time` is mapped to `string` to preserve the time value as-is.
46+
47+
When using the marshalling preset, the unmarshal function will:
48+
- Convert `date-time` and `date` values to `Date` objects using `new Date(value)`
49+
- Keep `time` values as strings without Date conversion
50+
51+
Check out this [example for a live demonstration of date and time handling](../../examples/typescript-generate-marshalling).
52+
3153
## Generate different `mapType`s for an `object`
3254

3355
Typescript offers different `mapType`s which can simplify the use based on the needs. This behavior can be changed through the `mapType` configuration.

examples/typescript-generate-marshalling/__snapshots__/index.spec.ts.snap

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,71 @@
22

33
exports[`Should be able to generate ts data model with marshal und unmarshal functions and should log expected output to console 1`] = `
44
Array [
5-
"class Test {
5+
"class Meeting {
66
private _email?: string;
7+
private _createdAt?: Date;
8+
private _meetingDate?: Date;
9+
private _meetingTime?: string;
710
811
constructor(input: {
912
email?: string,
13+
createdAt?: Date,
14+
meetingDate?: Date,
15+
meetingTime?: string,
1016
}) {
1117
this._email = input.email;
18+
this._createdAt = input.createdAt;
19+
this._meetingDate = input.meetingDate;
20+
this._meetingTime = input.meetingTime;
1221
}
1322
1423
get email(): string | undefined { return this._email; }
1524
set email(email: string | undefined) { this._email = email; }
1625
26+
get createdAt(): Date | undefined { return this._createdAt; }
27+
set createdAt(createdAt: Date | undefined) { this._createdAt = createdAt; }
28+
29+
get meetingDate(): Date | undefined { return this._meetingDate; }
30+
set meetingDate(meetingDate: Date | undefined) { this._meetingDate = meetingDate; }
31+
32+
get meetingTime(): string | undefined { return this._meetingTime; }
33+
set meetingTime(meetingTime: string | undefined) { this._meetingTime = meetingTime; }
34+
1735
public marshal() : string {
1836
let json = '{'
1937
if(this.email !== undefined) {
2038
json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`;
2139
}
40+
if(this.createdAt !== undefined) {
41+
json += \`\\"createdAt\\": \${typeof this.createdAt === 'number' || typeof this.createdAt === 'boolean' ? this.createdAt : JSON.stringify(this.createdAt)},\`;
42+
}
43+
if(this.meetingDate !== undefined) {
44+
json += \`\\"meetingDate\\": \${typeof this.meetingDate === 'number' || typeof this.meetingDate === 'boolean' ? this.meetingDate : JSON.stringify(this.meetingDate)},\`;
45+
}
46+
if(this.meetingTime !== undefined) {
47+
json += \`\\"meetingTime\\": \${typeof this.meetingTime === 'number' || typeof this.meetingTime === 'boolean' ? this.meetingTime : JSON.stringify(this.meetingTime)},\`;
48+
}
2249
2350
//Remove potential last comma
2451
return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`;
2552
}
2653
27-
public static unmarshal(json: string | object): Test {
54+
public static unmarshal(json: string | object): Meeting {
2855
const obj = typeof json === \\"object\\" ? json : JSON.parse(json);
29-
const instance = new Test({} as any);
56+
const instance = new Meeting({} as any);
3057
3158
if (obj[\\"email\\"] !== undefined) {
3259
instance.email = obj[\\"email\\"];
3360
}
61+
if (obj[\\"createdAt\\"] !== undefined) {
62+
instance.createdAt = obj[\\"createdAt\\"] == null ? undefined : new Date(obj[\\"createdAt\\"]);
63+
}
64+
if (obj[\\"meetingDate\\"] !== undefined) {
65+
instance.meetingDate = obj[\\"meetingDate\\"] == null ? undefined : new Date(obj[\\"meetingDate\\"]);
66+
}
67+
if (obj[\\"meetingTime\\"] !== undefined) {
68+
instance.meetingTime = obj[\\"meetingTime\\"];
69+
}
3470
3571
3672
return instance;

examples/typescript-generate-marshalling/index.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,28 @@ const generator = new TypeScriptGenerator({
1313
});
1414
const jsonSchemaDraft7 = {
1515
$schema: 'http://json-schema.org/draft-07/schema#',
16-
$id: 'Test',
16+
$id: 'Meeting',
1717
type: 'object',
1818
additionalProperties: false,
1919
properties: {
2020
email: {
2121
type: 'string',
2222
format: 'email'
23+
},
24+
// date-time and date map to Date type
25+
createdAt: {
26+
type: 'string',
27+
format: 'date-time'
28+
},
29+
meetingDate: {
30+
type: 'string',
31+
format: 'date'
32+
},
33+
// time maps to string (not Date) because time-only strings
34+
// like "14:30:00" are not valid Date constructor arguments
35+
meetingTime: {
36+
type: 'string',
37+
format: 'time'
2338
}
2439
}
2540
};

src/generators/typescript/TypeScriptConstrainer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ export const TypeScriptDefaultTypeMapping: TypeScriptTypeMapping = {
3232
},
3333
String({ constrainedModel }): string {
3434
const format = constrainedModel?.options?.format;
35-
if (format === 'date-time' || format === 'date' || format === 'time') {
35+
// Only date-time and date are valid Date constructor arguments
36+
// time (e.g., "14:30:00") is NOT valid, so keep as string
37+
if (format === 'date-time' || format === 'date') {
3638
return applyNullable(constrainedModel, 'Date');
3739
}
3840
return applyNullable(constrainedModel, 'string');

src/generators/typescript/presets/utils/UnmarshalFunction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ function renderUnmarshalProperty(
4040
}
4141

4242
// Date-typed properties need string→Date conversion
43+
// Note: 'time' is excluded - time-only strings (e.g., "14:30:00") are not valid Date constructor arguments
4344
if (
4445
model instanceof ConstrainedStringModel &&
45-
['date', 'date-time', 'time'].includes(model.options?.format ?? '')
46+
['date', 'date-time'].includes(model.options?.format ?? '')
4647
) {
4748
// Null check prevents new Date(null) → epoch date
4849
return `${modelInstanceVariable} == null ? ${nullValue} : new Date(${modelInstanceVariable})`;

test/generators/typescript/TypeScriptConstrainer.spec.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,53 @@ describe('TypeScriptConstrainer', () => {
8888
});
8989
expect(type).toEqual('string');
9090
});
91+
92+
test('should render Date type for format: date-time', () => {
93+
const model = new ConstrainedStringModel(
94+
'test',
95+
undefined,
96+
{ format: 'date-time' },
97+
''
98+
);
99+
const type = TypeScriptDefaultTypeMapping.String({
100+
constrainedModel: model,
101+
options: TypeScriptGenerator.defaultOptions,
102+
dependencyManager: undefined as never
103+
});
104+
expect(type).toEqual('Date');
105+
});
106+
107+
test('should render Date type for format: date', () => {
108+
const model = new ConstrainedStringModel(
109+
'test',
110+
undefined,
111+
{ format: 'date' },
112+
''
113+
);
114+
const type = TypeScriptDefaultTypeMapping.String({
115+
constrainedModel: model,
116+
options: TypeScriptGenerator.defaultOptions,
117+
dependencyManager: undefined as never
118+
});
119+
expect(type).toEqual('Date');
120+
});
121+
122+
test('should render string type for format: time', () => {
123+
// time-only strings (e.g., "14:30:00") are not valid Date constructor arguments
124+
// so format: time should map to string, not Date
125+
const model = new ConstrainedStringModel(
126+
'test',
127+
undefined,
128+
{ format: 'time' },
129+
''
130+
);
131+
const type = TypeScriptDefaultTypeMapping.String({
132+
constrainedModel: model,
133+
options: TypeScriptGenerator.defaultOptions,
134+
dependencyManager: undefined as never
135+
});
136+
expect(type).toEqual('string');
137+
});
91138
});
92139
describe('Boolean', () => {
93140
test('should render type', () => {

test/generators/typescript/preset/MarshallingPreset.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,9 @@ describe('Marshalling preset', () => {
154154
// Should use new Date() conversion for date format
155155
expect(result).toContain('new Date(obj["birthDate"])');
156156

157-
// Should use new Date() conversion for time format
158-
expect(result).toContain('new Date(obj["meetingTime"])');
157+
// Should NOT use new Date() for time format
158+
// time-only strings (e.g., "14:30:00") are not valid Date constructor arguments
159+
expect(result).not.toContain('new Date(obj["meetingTime"])');
159160

160161
// Should NOT use new Date() for regular strings
161162
expect(result).not.toContain('new Date(obj["regularString"])');

test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string
44
"class DateTest {
55
private _createdAt: Date;
66
private _birthDate?: Date;
7-
private _meetingTime?: Date;
7+
private _meetingTime?: string;
88
private _regularString?: string;
99
private _optionalDate?: Date;
1010
private _additionalProperties?: Map<string, any>;
1111
1212
constructor(input: {
1313
createdAt: Date,
1414
birthDate?: Date,
15-
meetingTime?: Date,
15+
meetingTime?: string,
1616
regularString?: string,
1717
optionalDate?: Date,
1818
additionalProperties?: Map<string, any>,
@@ -31,8 +31,8 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string
3131
get birthDate(): Date | undefined { return this._birthDate; }
3232
set birthDate(birthDate: Date | undefined) { this._birthDate = birthDate; }
3333
34-
get meetingTime(): Date | undefined { return this._meetingTime; }
35-
set meetingTime(meetingTime: Date | undefined) { this._meetingTime = meetingTime; }
34+
get meetingTime(): string | undefined { return this._meetingTime; }
35+
set meetingTime(meetingTime: string | undefined) { this._meetingTime = meetingTime; }
3636
3737
get regularString(): string | undefined { return this._regularString; }
3838
set regularString(regularString: string | undefined) { this._regularString = regularString; }
@@ -82,7 +82,7 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string
8282
instance.birthDate = obj[\\"birthDate\\"] == null ? undefined : new Date(obj[\\"birthDate\\"]);
8383
}
8484
if (obj[\\"meetingTime\\"] !== undefined) {
85-
instance.meetingTime = obj[\\"meetingTime\\"] == null ? undefined : new Date(obj[\\"meetingTime\\"]);
85+
instance.meetingTime = obj[\\"meetingTime\\"];
8686
}
8787
if (obj[\\"regularString\\"] !== undefined) {
8888
instance.regularString = obj[\\"regularString\\"];

0 commit comments

Comments
 (0)