Skip to content

Commit 42dc001

Browse files
authored
fix: date optional (#2460)
1 parent df70675 commit 42dc001

File tree

10 files changed

+279
-23
lines changed

10 files changed

+279
-23
lines changed

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import {
1717
*/
1818
function renderUnmarshalProperty(
1919
modelInstanceVariable: string,
20-
model: ConstrainedMetaModel
20+
model: ConstrainedMetaModel,
21+
isOptional: boolean = false
2122
) {
23+
const nullValue = isOptional ? 'undefined' : 'null';
2224
if (
2325
model instanceof ConstrainedReferenceModel &&
2426
!(model.ref instanceof ConstrainedEnumModel)
@@ -33,7 +35,7 @@ function renderUnmarshalProperty(
3335
!(model.valueModel instanceof ConstrainedUnionModel)
3436
) {
3537
return `${modelInstanceVariable} == null
36-
? null
38+
? ${nullValue}
3739
: ${modelInstanceVariable}.map((item: any) => ${model.valueModel.type}.unmarshal(item))`;
3840
}
3941

@@ -43,7 +45,7 @@ function renderUnmarshalProperty(
4345
['date', 'date-time', 'time'].includes(model.options?.format ?? '')
4446
) {
4547
// Null check prevents new Date(null) → epoch date
46-
return `${modelInstanceVariable} == null ? null : new Date(${modelInstanceVariable})`;
48+
return `${modelInstanceVariable} == null ? ${nullValue} : new Date(${modelInstanceVariable})`;
4749
}
4850

4951
return `${modelInstanceVariable}`;
@@ -58,9 +60,11 @@ function unmarshalRegularProperty(propModel: ConstrainedObjectPropertyModel) {
5860
}
5961

6062
const modelInstanceVariable = `obj["${propModel.unconstrainedPropertyName}"]`;
63+
const isOptional = propModel.required === false;
6164
const unmarshalCode = renderUnmarshalProperty(
6265
modelInstanceVariable,
63-
propModel.property
66+
propModel.property,
67+
isOptional
6468
);
6569
return `if (${modelInstanceVariable} !== undefined) {
6670
instance.${propModel.propertyName} = ${unmarshalCode};

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

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ const dateDoc = {
1818
required: ['createdAt']
1919
};
2020

21+
// Schema with nullable types (type: ['null', 'string']) - explicit null in type array
22+
const nullableDoc = {
23+
$id: 'NullableTest',
24+
type: 'object',
25+
properties: {
26+
// Nullable string (type includes null explicitly)
27+
nullableString: { type: ['null', 'string'] },
28+
// Nullable date (type includes null explicitly)
29+
nullableDate: { type: ['null', 'string'], format: 'date-time' },
30+
// Required nullable date (required but explicitly allows null)
31+
requiredNullableDate: { type: ['null', 'string'], format: 'date-time' },
32+
// Non-nullable required date for comparison
33+
requiredDate: { type: 'string', format: 'date-time' }
34+
},
35+
required: ['requiredNullableDate', 'requiredDate']
36+
};
37+
2138
const doc = {
2239
definitions: {
2340
NestedTest: {
@@ -147,7 +164,7 @@ describe('Marshalling preset', () => {
147164
expect(result).toMatchSnapshot();
148165
});
149166

150-
test('should handle null/undefined values for date properties', async () => {
167+
test('should return null for required date properties, undefined for optional', async () => {
151168
const generator = new TypeScriptGenerator({
152169
presets: [
153170
{
@@ -161,11 +178,63 @@ describe('Marshalling preset', () => {
161178
const models = await generator.generate(dateDoc);
162179
const result = models[0].result;
163180

164-
// Should include null safety check for date conversion
181+
// Required property (createdAt) should use null in unmarshal
165182
// Pattern: value == null ? null : new Date(value)
166183
expect(result).toMatch(
167184
/obj\["createdAt"\]\s*==\s*null\s*\?\s*null\s*:\s*new Date/
168185
);
186+
187+
// Optional property (optionalDate) should use undefined in unmarshal
188+
// Pattern: value == null ? undefined : new Date(value)
189+
expect(result).toMatch(
190+
/obj\["optionalDate"\]\s*==\s*null\s*\?\s*undefined\s*:\s*new Date/
191+
);
192+
});
193+
});
194+
195+
describe('nullable types (type: [null, string])', () => {
196+
test('should generate correct types and unmarshal for nullable properties', async () => {
197+
const generator = new TypeScriptGenerator({
198+
presets: [
199+
{
200+
preset: TS_COMMON_PRESET,
201+
options: {
202+
marshalling: true
203+
}
204+
}
205+
]
206+
});
207+
const models = await generator.generate(nullableDoc);
208+
expect(models).toHaveLength(1);
209+
const result = models[0].result;
210+
211+
// Snapshot for full verification
212+
expect(result).toMatchSnapshot();
213+
});
214+
215+
test('should handle nullable date types with proper null handling', async () => {
216+
const generator = new TypeScriptGenerator({
217+
presets: [
218+
{
219+
preset: TS_COMMON_PRESET,
220+
options: {
221+
marshalling: true
222+
}
223+
}
224+
]
225+
});
226+
const models = await generator.generate(nullableDoc);
227+
const result = models[0].result;
228+
229+
// Required non-nullable date should use null in unmarshal
230+
expect(result).toMatch(
231+
/obj\["requiredDate"\]\s*==\s*null\s*\?\s*null\s*:\s*new Date/
232+
);
233+
234+
// Nullable date properties should use Date conversion
235+
// Note: The behavior for nullable types (type: ['null', 'string'])
236+
// depends on how Modelina interprets them - as union types
237+
expect(result).toContain('new Date(');
169238
});
170239
});
171240
});

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

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,16 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string
7979
instance.createdAt = obj[\\"createdAt\\"] == null ? null : new Date(obj[\\"createdAt\\"]);
8080
}
8181
if (obj[\\"birthDate\\"] !== undefined) {
82-
instance.birthDate = obj[\\"birthDate\\"] == null ? null : new Date(obj[\\"birthDate\\"]);
82+
instance.birthDate = obj[\\"birthDate\\"] == null ? undefined : new Date(obj[\\"birthDate\\"]);
8383
}
8484
if (obj[\\"meetingTime\\"] !== undefined) {
85-
instance.meetingTime = obj[\\"meetingTime\\"] == null ? null : new Date(obj[\\"meetingTime\\"]);
85+
instance.meetingTime = obj[\\"meetingTime\\"] == null ? undefined : new Date(obj[\\"meetingTime\\"]);
8686
}
8787
if (obj[\\"regularString\\"] !== undefined) {
8888
instance.regularString = obj[\\"regularString\\"];
8989
}
9090
if (obj[\\"optionalDate\\"] !== undefined) {
91-
instance.optionalDate = obj[\\"optionalDate\\"] == null ? null : new Date(obj[\\"optionalDate\\"]);
91+
instance.optionalDate = obj[\\"optionalDate\\"] == null ? undefined : new Date(obj[\\"optionalDate\\"]);
9292
}
9393
9494
instance.additionalProperties = new Map();
@@ -101,6 +101,95 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string
101101
}"
102102
`;
103103
104+
exports[`Marshalling preset nullable types (type: [null, string]) should generate correct types and unmarshal for nullable properties 1`] = `
105+
"class NullableTest {
106+
private _nullableString?: string | null;
107+
private _nullableDate?: Date | null;
108+
private _requiredNullableDate: Date | null;
109+
private _requiredDate: Date;
110+
private _additionalProperties?: Map<string, any>;
111+
112+
constructor(input: {
113+
nullableString?: string | null,
114+
nullableDate?: Date | null,
115+
requiredNullableDate: Date | null,
116+
requiredDate: Date,
117+
additionalProperties?: Map<string, any>,
118+
}) {
119+
this._nullableString = input.nullableString;
120+
this._nullableDate = input.nullableDate;
121+
this._requiredNullableDate = input.requiredNullableDate;
122+
this._requiredDate = input.requiredDate;
123+
this._additionalProperties = input.additionalProperties;
124+
}
125+
126+
get nullableString(): string | null | undefined { return this._nullableString; }
127+
set nullableString(nullableString: string | null | undefined) { this._nullableString = nullableString; }
128+
129+
get nullableDate(): Date | null | undefined { return this._nullableDate; }
130+
set nullableDate(nullableDate: Date | null | undefined) { this._nullableDate = nullableDate; }
131+
132+
get requiredNullableDate(): Date | null { return this._requiredNullableDate; }
133+
set requiredNullableDate(requiredNullableDate: Date | null) { this._requiredNullableDate = requiredNullableDate; }
134+
135+
get requiredDate(): Date { return this._requiredDate; }
136+
set requiredDate(requiredDate: Date) { this._requiredDate = requiredDate; }
137+
138+
get additionalProperties(): Map<string, any> | undefined { return this._additionalProperties; }
139+
set additionalProperties(additionalProperties: Map<string, any> | undefined) { this._additionalProperties = additionalProperties; }
140+
141+
public marshal() : string {
142+
let json = '{'
143+
if(this.nullableString !== undefined) {
144+
json += \`\\"nullableString\\": \${typeof this.nullableString === 'number' || typeof this.nullableString === 'boolean' ? this.nullableString : JSON.stringify(this.nullableString)},\`;
145+
}
146+
if(this.nullableDate !== undefined) {
147+
json += \`\\"nullableDate\\": \${typeof this.nullableDate === 'number' || typeof this.nullableDate === 'boolean' ? this.nullableDate : JSON.stringify(this.nullableDate)},\`;
148+
}
149+
if(this.requiredNullableDate !== undefined) {
150+
json += \`\\"requiredNullableDate\\": \${typeof this.requiredNullableDate === 'number' || typeof this.requiredNullableDate === 'boolean' ? this.requiredNullableDate : JSON.stringify(this.requiredNullableDate)},\`;
151+
}
152+
if(this.requiredDate !== undefined) {
153+
json += \`\\"requiredDate\\": \${typeof this.requiredDate === 'number' || typeof this.requiredDate === 'boolean' ? this.requiredDate : JSON.stringify(this.requiredDate)},\`;
154+
}
155+
if(this.additionalProperties !== undefined) {
156+
for (const [key, value] of this.additionalProperties.entries()) {
157+
//Only unwrap those that are not already a property in the JSON object
158+
if([\\"nullableString\\",\\"nullableDate\\",\\"requiredNullableDate\\",\\"requiredDate\\",\\"additionalProperties\\"].includes(String(key))) continue;
159+
json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`;
160+
}
161+
}
162+
//Remove potential last comma
163+
return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`;
164+
}
165+
166+
public static unmarshal(json: string | object): NullableTest {
167+
const obj = typeof json === \\"object\\" ? json : JSON.parse(json);
168+
const instance = new NullableTest({} as any);
169+
170+
if (obj[\\"nullableString\\"] !== undefined) {
171+
instance.nullableString = obj[\\"nullableString\\"];
172+
}
173+
if (obj[\\"nullableDate\\"] !== undefined) {
174+
instance.nullableDate = obj[\\"nullableDate\\"] == null ? undefined : new Date(obj[\\"nullableDate\\"]);
175+
}
176+
if (obj[\\"requiredNullableDate\\"] !== undefined) {
177+
instance.requiredNullableDate = obj[\\"requiredNullableDate\\"] == null ? null : new Date(obj[\\"requiredNullableDate\\"]);
178+
}
179+
if (obj[\\"requiredDate\\"] !== undefined) {
180+
instance.requiredDate = obj[\\"requiredDate\\"] == null ? null : new Date(obj[\\"requiredDate\\"]);
181+
}
182+
183+
instance.additionalProperties = new Map();
184+
const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"nullableString\\",\\"nullableDate\\",\\"requiredNullableDate\\",\\"requiredDate\\",\\"additionalProperties\\"].includes(key);}));
185+
for (const [key, value] of propsToCheck) {
186+
instance.additionalProperties.set(key, value as any);
187+
}
188+
return instance;
189+
}
190+
}"
191+
`;
192+
104193
exports[`Marshalling preset should render un/marshal code 1`] = `
105194
"class Test {
106195
private _stringProp: string;
@@ -273,7 +362,7 @@ exports[`Marshalling preset should render un/marshal code 1`] = `
273362
}
274363
if (obj[\\"arrayTest\\"] !== undefined) {
275364
instance.arrayTest = obj[\\"arrayTest\\"] == null
276-
? null
365+
? undefined
277366
: obj[\\"arrayTest\\"].map((item: any) => NestedTest.unmarshal(item));
278367
}
279368
if (obj[\\"primitiveArrayTest\\"] !== undefined) {

test/runtime/generic-input-all.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,26 @@
7171
},
7272
"enum_type": {
7373
"enum": ["test", 1, true, {"test": 2}]
74+
},
75+
"nullable_string": {
76+
"type": ["null", "string"],
77+
"description": "Optional nullable string (type includes null explicitly)"
78+
},
79+
"nullable_date": {
80+
"type": ["null", "string"],
81+
"format": "date-time",
82+
"description": "Optional nullable date (type includes null explicitly)"
83+
},
84+
"required_nullable_date": {
85+
"type": ["null", "string"],
86+
"format": "date-time",
87+
"description": "Required nullable date (required but explicitly allows null)"
7488
}
7589
},
7690
"patternProperties": {
7791
"^S(.?*)test&": {
7892
"type": "string"
7993
}
8094
},
81-
"required": ["string_type", "boolean_type"]
95+
"required": ["string_type", "boolean_type", "required_nullable_date"]
8296
}

test/runtime/runtime-typescript/package-lock.json

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{
22
"scripts": {
3-
"test": "jest"
3+
"typecheck": "tsc --noEmit",
4+
"test": "npm run typecheck && jest"
45
},
56
"dependencies": {
67
"jsonbinpack": "1.1.2",
78
"jest": "^27.2.5",
89
"@swc/core": "^1.3.5",
9-
"@swc/jest": "^0.2.23"
10+
"@swc/jest": "^0.2.23",
11+
"typescript": "~5.6.0"
1012
}
11-
}
13+
}

test/runtime/runtime-typescript/test/DefaultAddressTest.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ describe('Default exports', () => {
1717
additionalProperties: new Map(Object.entries({"test": "test"})),
1818
enumType: EnumType.CURLYLEFT_QUOTATION_TEST_QUOTATION_COLON_2_CURLYRIGHT,
1919
tupleType: ['test', 1],
20-
unionType: 'test'
20+
unionType: 'test',
21+
requiredNullableDate: new Date('2023-01-01T00:00:00Z')
2122
});
2223
expect(testObject.stringType).toEqual('test');
2324
expect(testObject.numberType).toEqual(1);

0 commit comments

Comments
 (0)