diff --git a/docs/languages/TypeScript.md b/docs/languages/TypeScript.md index 288c3effb8..1a0be9d056 100644 --- a/docs/languages/TypeScript.md +++ b/docs/languages/TypeScript.md @@ -102,7 +102,34 @@ Here are all the supported presets and the libraries they use for converting to #### Generate marshalling and unmarshalling functions -Using the preset `TS_COMMON_PRESET` with the option `marshalling` to `true`, renders two function for the class models. One which convert the model to JSON and another which convert the model from JSON to an instance of the class. +Using the preset `TS_COMMON_PRESET` with the option `marshalling` to `true`, renders four methods for class models: + +| Method | Description | +|--------|-------------| +| `toJson(): Record` | Converts the instance to a plain JSON-serializable object | +| `marshal(): string` | Converts the instance to a JSON string (calls `JSON.stringify(this.toJson())`) | +| `static fromJson(obj: Record): ClassName` | Creates an instance from a plain JSON object | +| `static unmarshal(json: string \| object): ClassName` | Creates an instance from a JSON string or object (calls `fromJson()` internally) | + +**When to use each method:** + +- Use `toJson()`/`fromJson()` when working with objects directly (e.g., API responses already parsed, or when you need to manipulate the data before serialization) +- Use `marshal()`/`unmarshal()` when working with JSON strings + +**Example:** + +```typescript +// Create an instance +const meeting = new Meeting({ email: 'test@example.com', createdAt: new Date() }); + +// Object conversion +const obj = meeting.toJson(); // Returns plain object +const fromObj = Meeting.fromJson(obj); // Creates instance from object + +// String serialization +const str = meeting.marshal(); // Returns JSON string +const fromStr = Meeting.unmarshal(str); // Creates instance from string +``` Check out this [example out for a live demonstration](../../examples/typescript-generate-marshalling). diff --git a/examples/typescript-generate-jsonbinpack/__snapshots__/index.spec.ts.snap b/examples/typescript-generate-jsonbinpack/__snapshots__/index.spec.ts.snap index c6f73d2be8..8ca642d197 100644 --- a/examples/typescript-generate-jsonbinpack/__snapshots__/index.spec.ts.snap +++ b/examples/typescript-generate-jsonbinpack/__snapshots__/index.spec.ts.snap @@ -14,27 +14,34 @@ Array [ get email(): string | undefined { return this._email; } set email(email: string | undefined) { this._email = email; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.email !== undefined) { - json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`; + json[\\"email\\"] = this.email; } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): Test { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Test { const instance = new Test({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; } - + return instance; } + + public static unmarshal(json: string | object): Test { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Test.fromJson(obj as Record); + } public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"$schema\\":\\"https://json-schema.org/draft/2020-12/schema\\",\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"format\\":\\"email\\",\\"type\\":\\"string\\",\\"x-modelgen-inferred-name\\":\\"email\\"}},\\"additionalProperties\\":false,\\"$id\\":\\"Test\\",\\"x-modelgen-inferred-name\\":\\"root\\"}); diff --git a/examples/typescript-generate-marshalling/README.md b/examples/typescript-generate-marshalling/README.md index b72bcd233f..a77d125c23 100644 --- a/examples/typescript-generate-marshalling/README.md +++ b/examples/typescript-generate-marshalling/README.md @@ -1,6 +1,45 @@ # TypeScript Data Models with un/marshalling functionality -A basic example of how to use the un/marshalling functionality of the typescript class. +A basic example of how to use the un/marshalling functionality of the TypeScript class. + +## Generated Methods + +When `marshalling: true` is enabled, the following methods are generated: + +### `toJson(): Record` +Converts the instance to a plain JSON-serializable object. Useful when you need to work with the data as an object before serialization. + +### `marshal(): string` +Converts the instance to a JSON string. Internally calls `JSON.stringify(this.toJson())`. + +### `static fromJson(obj: Record): ClassName` +Creates an instance from a plain JSON object. Useful when you receive data as an object (e.g., from an API response already parsed). + +### `static unmarshal(json: string | object): ClassName` +Creates an instance from either a JSON string or object. For strings, it parses first then calls `fromJson()`. For objects, it directly calls `fromJson()`. + +## Example Usage + +```typescript +// Create an instance +const meeting = new Meeting({ + email: 'test@example.com', + createdAt: new Date() +}); + +// Convert to plain object (for manipulation or custom serialization) +const jsonObject = meeting.toJson(); +console.log(jsonObject); // { email: 'test@example.com', createdAt: '2023-01-01T00:00:00.000Z' } + +// Convert to JSON string +const jsonString = meeting.marshal(); + +// Create from plain object +const fromObj = Meeting.fromJson(jsonObject); + +// Create from JSON string +const fromString = Meeting.unmarshal(jsonString); +``` ## How to run this example diff --git a/examples/typescript-generate-marshalling/__snapshots__/index.spec.ts.snap b/examples/typescript-generate-marshalling/__snapshots__/index.spec.ts.snap index 73a4643930..23534081a1 100644 --- a/examples/typescript-generate-marshalling/__snapshots__/index.spec.ts.snap +++ b/examples/typescript-generate-marshalling/__snapshots__/index.spec.ts.snap @@ -32,45 +32,52 @@ Array [ get meetingTime(): string | undefined { return this._meetingTime; } set meetingTime(meetingTime: string | undefined) { this._meetingTime = meetingTime; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.email !== undefined) { - json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`; + json[\\"email\\"] = this.email; } if(this.createdAt !== undefined) { - json += \`\\"createdAt\\": \${typeof this.createdAt === 'number' || typeof this.createdAt === 'boolean' ? this.createdAt : JSON.stringify(this.createdAt)},\`; + json[\\"createdAt\\"] = this.createdAt; } if(this.meetingDate !== undefined) { - json += \`\\"meetingDate\\": \${typeof this.meetingDate === 'number' || typeof this.meetingDate === 'boolean' ? this.meetingDate : JSON.stringify(this.meetingDate)},\`; + json[\\"meetingDate\\"] = this.meetingDate; } if(this.meetingTime !== undefined) { - json += \`\\"meetingTime\\": \${typeof this.meetingTime === 'number' || typeof this.meetingTime === 'boolean' ? this.meetingTime : JSON.stringify(this.meetingTime)},\`; + json[\\"meetingTime\\"] = this.meetingTime; } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): Meeting { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Meeting { const instance = new Meeting({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; } if (obj[\\"createdAt\\"] !== undefined) { - instance.createdAt = obj[\\"createdAt\\"] == null ? undefined : new Date(obj[\\"createdAt\\"]); + instance.createdAt = obj[\\"createdAt\\"] == null ? undefined : new Date(obj[\\"createdAt\\"] as string); } if (obj[\\"meetingDate\\"] !== undefined) { - instance.meetingDate = obj[\\"meetingDate\\"] == null ? undefined : new Date(obj[\\"meetingDate\\"]); + instance.meetingDate = obj[\\"meetingDate\\"] == null ? undefined : new Date(obj[\\"meetingDate\\"] as string); } if (obj[\\"meetingTime\\"] !== undefined) { instance.meetingTime = obj[\\"meetingTime\\"]; } - + return instance; } + + public static unmarshal(json: string | object): Meeting { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Meeting.fromJson(obj as Record); + } }", ] `; diff --git a/src/generators/typescript/presets/CommonPreset.ts b/src/generators/typescript/presets/CommonPreset.ts index 5b44ebcb42..8160d27ff6 100644 --- a/src/generators/typescript/presets/CommonPreset.ts +++ b/src/generators/typescript/presets/CommonPreset.ts @@ -1,7 +1,7 @@ import { TypeScriptPreset } from '../TypeScriptPreset'; import renderExampleFunction from './utils/ExampleFunction'; -import { renderUnmarshal } from './utils/UnmarshalFunction'; -import { renderMarshal } from './utils/MarshalFunction'; +import { renderFromJson, renderUnmarshal } from './utils/UnmarshalFunction'; +import { renderMarshal, renderToJson } from './utils/MarshalFunction'; export interface TypeScriptCommonPresetOptions { marshalling: boolean; @@ -9,7 +9,7 @@ export interface TypeScriptCommonPresetOptions { } /** - * Preset which adds `marshal`, `unmarshal`, `example` functions to class. + * Preset which adds `toJson`, `marshal`, `fromJson`, `unmarshal`, `example` functions to class. * * @implements {TypeScriptPreset} */ @@ -21,8 +21,12 @@ export const TS_COMMON_PRESET: TypeScriptPreset = const blocks: string[] = []; if (options.marshalling === true) { - blocks.push(renderMarshal({ renderer, model })); - blocks.push(renderUnmarshal({ renderer, model })); + blocks.push( + renderToJson({ renderer, model }), + renderMarshal(), + renderFromJson({ renderer, model }), + renderUnmarshal({ renderer, model }) + ); } if (options.example === true) { diff --git a/src/generators/typescript/presets/utils/MarshalFunction.ts b/src/generators/typescript/presets/utils/MarshalFunction.ts index 00b9dcce70..36950ee99b 100644 --- a/src/generators/typescript/presets/utils/MarshalFunction.ts +++ b/src/generators/typescript/presets/utils/MarshalFunction.ts @@ -16,233 +16,221 @@ import { ConstrainedUnionModel } from '../../../../models'; -function realizePropertyFactory(prop: string) { - return `$\{typeof ${prop} === 'number' || typeof ${prop} === 'boolean' ? ${prop} : JSON.stringify(${prop})}`; -} - -function renderMarshalProperty( +/** + * Render toJson property conversion - returns the value to assign to the JSON object property + */ +function renderToJsonProperty( modelInstanceVariable: string, model: ConstrainedMetaModel -) { +): string { if ( model instanceof ConstrainedReferenceModel && !(model.ref instanceof ConstrainedEnumModel) ) { - // Runtime check for .marshal() method to handle plain objects passed to constructor - return `$\{${modelInstanceVariable} && typeof ${modelInstanceVariable} === 'object' && 'marshal' in ${modelInstanceVariable} && typeof ${modelInstanceVariable}.marshal === 'function' ? ${modelInstanceVariable}.marshal() : JSON.stringify(${modelInstanceVariable})}`; + // Runtime check for .toJson() method to handle plain objects passed to constructor + return `${modelInstanceVariable} && typeof ${modelInstanceVariable} === 'object' && 'toJson' in ${modelInstanceVariable} && typeof ${modelInstanceVariable}.toJson === 'function' ? ${modelInstanceVariable}.toJson() : ${modelInstanceVariable}`; } - return realizePropertyFactory(modelInstanceVariable); + return modelInstanceVariable; } /** - * Render marshalling logic for tuples + * Render `marshal` function based on model - delegates to toJson() */ -function renderTupleSerialization( +export function renderMarshal(): string { + return `public marshal(): string { + return JSON.stringify(this.toJson()); +}`; +} + +/** + * Render toJson logic for tuples - returns array with converted values + */ +function renderToJsonTuple( modelInstanceVariable: string, unconstrainedProperty: string, tuple: ConstrainedTupleModel -) { - const t = tuple.tuple.map((tupleEntry) => { - const temp = renderMarshalProperty( - `${modelInstanceVariable}[${tupleEntry.index}]`, - tupleEntry.value - ); - return `if(${modelInstanceVariable}[${tupleEntry.index}]) { - serializedTuple[${tupleEntry.index}] = \`${temp}\` -} else { - serializedTuple[${tupleEntry.index}] = null; -}`; +): string { + const tupleAssignments = tuple.tuple.map((tupleEntry) => { + const itemVar = `${modelInstanceVariable}[${tupleEntry.index}]`; + const itemConversion = renderToJsonProperty(itemVar, tupleEntry.value); + return `${itemVar} !== undefined ? ${itemConversion} : null`; }); - return `const serializedTuple: any[] = []; -${t.join('\n')} -json += \`"${unconstrainedProperty}": [\${serializedTuple.join(',')}],\`;`; + return `json["${unconstrainedProperty}"] = [${tupleAssignments.join(', ')}];`; } /** - * Render marshalling logic for unions + * Render toJson logic for arrays of unions */ -function renderUnionSerializationArray( +function renderToJsonUnionArray( modelInstanceVariable: string, - prop: string, unconstrainedProperty: string, unionModel: ConstrainedUnionModel -) { - const propName = `${prop}JsonValues`; - const allUnionReferences = unionModel.union - .filter((model) => { - return ( - model instanceof ConstrainedReferenceModel && - !(model.ref instanceof ConstrainedEnumModel) - ); - }) - .map((model) => { - return `unionItem instanceof ${model.type}`; - }); - const hasUnionReference = allUnionReferences.length > 0; - let unionSerialization = `${propName}.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem))`; +): string { + const hasUnionReference = unionModel.union.some( + (model) => + model instanceof ConstrainedReferenceModel && + !(model.ref instanceof ConstrainedEnumModel) + ); + if (hasUnionReference) { - // Runtime check for .marshal() method to handle plain objects - unionSerialization = `if(unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function') { - ${propName}.push(unionItem.marshal()); - } else { - ${propName}.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)) - }`; - } - return `const ${propName}: any[] = []; - for (const unionItem of ${modelInstanceVariable}) { - ${unionSerialization} + return `json["${unconstrainedProperty}"] = ${modelInstanceVariable}.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + );`; } - json += \`"${unconstrainedProperty}": [\${${propName}.join(',')}],\`;`; + + return `json["${unconstrainedProperty}"] = ${modelInstanceVariable};`; } /** - * Render marshalling logic for Arrays + * Render toJson logic for arrays */ -function renderArraySerialization( +function renderToJsonArray( modelInstanceVariable: string, - prop: string, unconstrainedProperty: string, arrayModel: ConstrainedArrayModel -) { - const propName = `${prop}JsonValues`; - return `let ${propName}: any[] = []; - for (const unionItem of ${modelInstanceVariable}) { - ${propName}.push(\`${renderMarshalProperty( - 'unionItem', - arrayModel.valueModel - )}\`); +): string { + if ( + arrayModel.valueModel instanceof ConstrainedReferenceModel && + !(arrayModel.valueModel.ref instanceof ConstrainedEnumModel) + ) { + return `json["${unconstrainedProperty}"] = ${modelInstanceVariable}.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + );`; } - json += \`"${unconstrainedProperty}": [\${${propName}.join(',')}],\`;`; + + return `json["${unconstrainedProperty}"] = ${modelInstanceVariable};`; } /** - * Render marshalling logic for unions + * Render toJson logic for unions */ -function renderUnionSerialization( +function renderToJsonUnion( modelInstanceVariable: string, unconstrainedProperty: string, unionModel: ConstrainedUnionModel -) { - const allUnionReferences = unionModel.union - .filter((model) => { - return ( - model instanceof ConstrainedReferenceModel && - !(model.ref instanceof ConstrainedEnumModel) - ); - }) - .map((model) => { - return `${modelInstanceVariable} instanceof ${model.type}`; - }); - const hasUnionReference = allUnionReferences.length > 0; +): string { + const hasUnionReference = unionModel.union.some( + (model) => + model instanceof ConstrainedReferenceModel && + !(model.ref instanceof ConstrainedEnumModel) + ); + if (hasUnionReference) { - // Runtime check for .marshal() method to handle plain objects - return `if(${modelInstanceVariable} && typeof ${modelInstanceVariable} === 'object' && 'marshal' in ${modelInstanceVariable} && typeof ${modelInstanceVariable}.marshal === 'function') { - json += \`"${unconstrainedProperty}": $\{${modelInstanceVariable}.marshal()},\`; - } else { - json += \`"${unconstrainedProperty}": ${realizePropertyFactory( - modelInstanceVariable - )},\`; - }`; + return `if(${modelInstanceVariable} && typeof ${modelInstanceVariable} === 'object' && 'toJson' in ${modelInstanceVariable} && typeof ${modelInstanceVariable}.toJson === 'function') { + json["${unconstrainedProperty}"] = ${modelInstanceVariable}.toJson(); + } else { + json["${unconstrainedProperty}"] = ${modelInstanceVariable}; + }`; } - return `json += \`"${unconstrainedProperty}": ${realizePropertyFactory( - modelInstanceVariable - )},\`;`; + + return `json["${unconstrainedProperty}"] = ${modelInstanceVariable};`; } /** - * Render marshalling logic for dictionary types + * Render toJson logic for dictionary types (additionalProperties) */ -function renderDictionarySerialization( +function renderToJsonDictionary( properties: Record -) { +): string[] { const unwrapDictionaryProperties = getDictionary(properties); const originalPropertyNames = getOriginalPropertyList(properties); + return unwrapDictionaryProperties.map(([prop, propModel]) => { - let dictionaryValueType; - if ( - (propModel.property as ConstrainedDictionaryModel).value instanceof - ConstrainedUnionModel - ) { - dictionaryValueType = renderUnionSerialization( - 'value', - '${key}', - (propModel.property as ConstrainedDictionaryModel) - .value as ConstrainedUnionModel + const dictValue = (propModel.property as ConstrainedDictionaryModel).value; + let valueConversion: string; + + if (dictValue instanceof ConstrainedUnionModel) { + const hasUnionReference = dictValue.union.some( + (model) => + model instanceof ConstrainedReferenceModel && + !(model.ref instanceof ConstrainedEnumModel) ); + if (hasUnionReference) { + valueConversion = `value && typeof value === 'object' && 'toJson' in value && typeof value.toJson === 'function' ? value.toJson() : value`; + } else { + valueConversion = 'value'; + } + } else if ( + dictValue instanceof ConstrainedReferenceModel && + !(dictValue.ref instanceof ConstrainedEnumModel) + ) { + valueConversion = `value && typeof value === 'object' && 'toJson' in value && typeof value.toJson === 'function' ? value.toJson() : value`; } else { - const type = renderMarshalProperty('value', propModel.property); - dictionaryValueType = `json += \`"$\{key}": ${type},\`;`; + valueConversion = 'value'; } - return `if(this.${prop} !== undefined) { + + return `if(this.${prop} !== undefined) { for (const [key, value] of this.${prop}.entries()) { //Only unwrap those that are not already a property in the JSON object - if([${originalPropertyNames - .map((value) => `"${value}"`) - .join(',')}].includes(String(key))) continue; - ${dictionaryValueType} + if([${originalPropertyNames.map((v) => `"${v}"`).join(',')}].includes(String(key))) continue; + json[key] = ${valueConversion}; } }`; }); } /** - * Render marshalling code for all the normal properties (not dictionaries with unwrap) + * Render toJson code for all normal properties (not dictionaries with unwrap) */ -function renderNormalProperties( +function renderToJsonNormalProperties( properties: Record -) { +): string[] { const normalProperties = getNormalProperties(properties); return normalProperties.map(([prop, propModel]) => { const modelInstanceVariable = `this.${prop}`; - let marshalCode; + let toJsonCode: string; + if ( propModel.property instanceof ConstrainedArrayModel && propModel.property.valueModel instanceof ConstrainedUnionModel ) { - marshalCode = renderUnionSerializationArray( + toJsonCode = renderToJsonUnionArray( modelInstanceVariable, - prop, propModel.unconstrainedPropertyName, propModel.property.valueModel ); } else if (propModel.property instanceof ConstrainedUnionModel) { - marshalCode = renderUnionSerialization( + toJsonCode = renderToJsonUnion( modelInstanceVariable, propModel.unconstrainedPropertyName, propModel.property ); } else if (propModel.property instanceof ConstrainedArrayModel) { - marshalCode = renderArraySerialization( + toJsonCode = renderToJsonArray( modelInstanceVariable, - prop, propModel.unconstrainedPropertyName, propModel.property ); } else if (propModel.property instanceof ConstrainedTupleModel) { - marshalCode = renderTupleSerialization( + toJsonCode = renderToJsonTuple( modelInstanceVariable, propModel.unconstrainedPropertyName, propModel.property ); } else { - const propMarshalCode = renderMarshalProperty( + const propToJsonCode = renderToJsonProperty( modelInstanceVariable, propModel.property ); - marshalCode = `json += \`"${propModel.unconstrainedPropertyName}": ${propMarshalCode},\`;`; + toJsonCode = `json["${propModel.unconstrainedPropertyName}"] = ${propToJsonCode};`; } + return `if(${modelInstanceVariable} !== undefined) { - ${marshalCode} + ${toJsonCode} }`; }); } /** - * Render `marshal` function based on model + * Render `toJson` function based on model */ -export function renderMarshal({ +export function renderToJson({ renderer, model }: { @@ -250,15 +238,13 @@ export function renderMarshal({ model: ConstrainedObjectModel; }): string { const properties = model.properties || {}; - const marshalNormalProperties = renderNormalProperties(properties); - const marshalUnwrapDictionaryProperties = - renderDictionarySerialization(properties); + const toJsonNormalProperties = renderToJsonNormalProperties(properties); + const toJsonDictionaryProperties = renderToJsonDictionary(properties); - return `public marshal() : string { - let json = '{' -${renderer.indent(marshalNormalProperties.join('\n'))} -${renderer.indent(marshalUnwrapDictionaryProperties.join('\n'))} - //Remove potential last comma - return \`$\{json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return `public toJson(): Record { + const json: Record = {}; +${renderer.indent(toJsonNormalProperties.join('\n'))} +${renderer.indent(toJsonDictionaryProperties.join('\n'))} + return json; }`; } diff --git a/src/generators/typescript/presets/utils/UnmarshalFunction.ts b/src/generators/typescript/presets/utils/UnmarshalFunction.ts index a1fc93aed3..1d86695602 100644 --- a/src/generators/typescript/presets/utils/UnmarshalFunction.ts +++ b/src/generators/typescript/presets/utils/UnmarshalFunction.ts @@ -13,19 +13,19 @@ import { } from '../../../../models'; /** - * Render the unmarshalled value + * Render the fromJson property value - uses .fromJson() for nested models */ -function renderUnmarshalProperty( +function renderFromJsonProperty( modelInstanceVariable: string, model: ConstrainedMetaModel, isOptional: boolean = false -) { +): string { const nullValue = isOptional ? 'undefined' : 'null'; if ( model instanceof ConstrainedReferenceModel && !(model.ref instanceof ConstrainedEnumModel) ) { - return `${model.type}.unmarshal(${modelInstanceVariable})`; + return `${model.type}.fromJson(${modelInstanceVariable} as Record)`; } if ( @@ -36,7 +36,7 @@ function renderUnmarshalProperty( ) { return `${modelInstanceVariable} == null ? ${nullValue} - : ${modelInstanceVariable}.map((item: any) => ${model.valueModel.type}.unmarshal(item))`; + : (${modelInstanceVariable} as Record[]).map((item: Record) => ${model.valueModel.type}.fromJson(item))`; } // Date-typed properties need string→Date conversion @@ -46,38 +46,40 @@ function renderUnmarshalProperty( ['date', 'date-time'].includes(model.options?.format ?? '') ) { // Null check prevents new Date(null) → epoch date - return `${modelInstanceVariable} == null ? ${nullValue} : new Date(${modelInstanceVariable})`; + return `${modelInstanceVariable} == null ? ${nullValue} : new Date(${modelInstanceVariable} as string)`; } return `${modelInstanceVariable}`; } /** - * Render the code for unmarshalling of regular properties + * Render the code for fromJson of regular properties */ -function unmarshalRegularProperty(propModel: ConstrainedObjectPropertyModel) { +function fromJsonRegularProperty( + propModel: ConstrainedObjectPropertyModel +): string | undefined { if (propModel.property.options.const) { return undefined; } const modelInstanceVariable = `obj["${propModel.unconstrainedPropertyName}"]`; const isOptional = propModel.required === false; - const unmarshalCode = renderUnmarshalProperty( + const fromJsonCode = renderFromJsonProperty( modelInstanceVariable, propModel.property, isOptional ); return `if (${modelInstanceVariable} !== undefined) { - instance.${propModel.propertyName} = ${unmarshalCode}; + instance.${propModel.propertyName} = ${fromJsonCode}; }`; } /** - * Render the code for unmarshalling unwrappable dictionary models + * Render the code for fromJson of unwrappable dictionary models */ -function unmarshalDictionary(model: ConstrainedObjectModel) { - const setDictionaryProperties = []; - const unmarshalDictionaryProperties = []; +function fromJsonDictionary(model: ConstrainedObjectModel): string { + const setDictionaryProperties: string[] = []; + const fromJsonDictionaryProperties: string[] = []; const properties = model.properties || {}; const propertyKeys = [...Object.entries(properties)]; const originalPropertyNames = propertyKeys.map(([, model]) => { @@ -86,14 +88,14 @@ function unmarshalDictionary(model: ConstrainedObjectModel) { const unwrapDictionaryProperties = getDictionary(properties); for (const [prop, propModel] of unwrapDictionaryProperties) { - const modelInstanceVariable = 'value as any'; - const unmarshalCode = renderUnmarshalProperty( + const modelInstanceVariable = 'value'; + const fromJsonCode = renderFromJsonProperty( modelInstanceVariable, (propModel.property as ConstrainedDictionaryModel).value ); setDictionaryProperties.push(`instance.${prop} = new Map();`); - unmarshalDictionaryProperties.push( - `instance.${prop}.set(key, ${unmarshalCode});` + fromJsonDictionaryProperties.push( + `instance.${prop}.set(key, ${fromJsonCode});` ); } @@ -104,16 +106,16 @@ function unmarshalDictionary(model: ConstrainedObjectModel) { return `${setDictionaryProperties.join('\n')} const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![${corePropertyKeys}].includes(key);})); for (const [key, value] of propsToCheck) { - ${unmarshalDictionaryProperties.join('\n')} + ${fromJsonDictionaryProperties.join('\n')} }`; } return ''; } /** - * Render `unmarshal` function based on model + * Render `fromJson` function based on model */ -export function renderUnmarshal({ +export function renderFromJson({ renderer, model }: { @@ -122,18 +124,32 @@ export function renderUnmarshal({ }): string { const properties = model.properties || {}; const normalProperties = getNormalProperties(properties); - const unmarshalNormalProperties = normalProperties.map(([, propModel]) => - unmarshalRegularProperty(propModel) + const fromJsonNormalProperties = normalProperties.map(([, propModel]) => + fromJsonRegularProperty(propModel) ); - const unwrappedDictionaryCode = unmarshalDictionary(model); + const fromJsonDictionaryCode = fromJsonDictionary(model); - return `public static unmarshal(json: string | object): ${model.type} { - const obj = typeof json === "object" ? json : JSON.parse(json); + return `public static fromJson(obj: Record): ${model.type} { const instance = new ${model.type}({} as any); -${renderer.indent(unmarshalNormalProperties.join('\n'))} - -${renderer.indent(unwrappedDictionaryCode)} +${renderer.indent(fromJsonNormalProperties.join('\n'))} + +${renderer.indent(fromJsonDictionaryCode)} return instance; }`; } + +/** + * Render `unmarshal` function based on model - delegates to fromJson() + */ +export function renderUnmarshal({ + model +}: { + renderer: ClassRenderer; + model: ConstrainedObjectModel; +}): string { + return `public static unmarshal(json: string | object): ${model.type} { + const obj = typeof json === "object" ? json : JSON.parse(json); + return ${model.type}.fromJson(obj as Record); +}`; +} diff --git a/test/generators/typescript/preset/MarshallingPreset.spec.ts b/test/generators/typescript/preset/MarshallingPreset.spec.ts index a4e6d0c4ad..c327162f14 100644 --- a/test/generators/typescript/preset/MarshallingPreset.spec.ts +++ b/test/generators/typescript/preset/MarshallingPreset.spec.ts @@ -132,6 +132,176 @@ describe('Marshalling preset', () => { expect(models[2].result).toMatchSnapshot(); }); + describe('toJson/fromJson methods', () => { + test('should render toJson method that returns Record', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // Should have toJson method with correct signature + expect(result).toContain('public toJson(): Record'); + + // Should return an object, not a string + expect(result).toMatch(/public toJson\(\): Record\s*\{[\s\S]*?return json;[\s\S]*?\}/); + }); + + test('should render fromJson static method that accepts Record', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // Should have fromJson static method with correct signature + expect(result).toContain( + 'public static fromJson(obj: Record): Test' + ); + }); + + test('should render marshal method that calls toJson', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // marshal() should delegate to toJson() + expect(result).toContain('JSON.stringify(this.toJson())'); + }); + + test('should render unmarshal method that calls fromJson', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // unmarshal() should delegate to fromJson() + expect(result).toContain('.fromJson('); + }); + + test('should render toJson with nested model calling .toJson()', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // For nested objects, toJson should call .toJson() on the nested model + expect(result).toMatch(/nestedObject.*\.toJson\(\)/); + }); + + test('should render fromJson with nested model calling .fromJson()', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // For nested objects, fromJson should call .fromJson() on the nested model + expect(result).toContain('NestedTest.fromJson('); + }); + + test('should render toJson for arrays of models calling .toJson()', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // For arrays of models, toJson should map items with .toJson() + expect(result).toMatch(/\.map\([\s\S]*?\.toJson\(\)/); + }); + + test('should render fromJson for arrays of models calling .fromJson()', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // For arrays of models, fromJson should map items with .fromJson() + // Check in fromJson context + expect(result).toMatch(/fromJson[\s\S]*?\.map\(.*NestedTest\.fromJson/); + }); + + test('should render complete toJson/fromJson/marshal/unmarshal methods', async () => { + const generator = new TypeScriptGenerator({ + presets: [ + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + } + ] + }); + const models = await generator.generate(doc); + const result = models[0].result; + + // Snapshot for full generated output verification + expect(result).toMatchSnapshot(); + }); + }); + describe('date unmarshal', () => { test('should convert date-formatted string properties to Date objects in unmarshal', async () => { const generator = new TypeScriptGenerator({ @@ -149,17 +319,17 @@ describe('Marshalling preset', () => { const result = models[0].result; // Should use new Date() conversion for date-time format - expect(result).toContain('new Date(obj["createdAt"])'); + expect(result).toMatch(/new Date\(obj\["createdAt"\]/); // Should use new Date() conversion for date format - expect(result).toContain('new Date(obj["birthDate"])'); + expect(result).toMatch(/new Date\(obj\["birthDate"\]/); // Should NOT use new Date() for time format // time-only strings (e.g., "14:30:00") are not valid Date constructor arguments - expect(result).not.toContain('new Date(obj["meetingTime"])'); + expect(result).not.toMatch(/new Date\(obj\["meetingTime"\]/); // Should NOT use new Date() for regular strings - expect(result).not.toContain('new Date(obj["regularString"])'); + expect(result).not.toMatch(/new Date\(obj\["regularString"\]/); // Snapshot for full verification expect(result).toMatchSnapshot(); diff --git a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap index 5ad476c611..7a3d0e5b88 100644 --- a/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/JsonBinPackPreset.spec.ts.snap @@ -19,37 +19,44 @@ exports[`JsonBinPack preset should work fine with AsyncAPI inputs 1`] = ` get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.email !== undefined) { - json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`; + json[\\"email\\"] = this.email; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"email\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): UserSignedupPayload { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): UserSignedupPayload { const instance = new UserSignedupPayload({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"email\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): UserSignedupPayload { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return UserSignedupPayload.fromJson(obj as Record); + } public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"$schema\\":\\"https://json-schema.org/draft/2020-12/schema\\",\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"format\\":\\"email\\",\\"type\\":\\"string\\",\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"UserSignedupPayloadEmail\\"}},\\"x-parser-schema-id\\":\\"\\",\\"x-modelgen-inferred-name\\":\\"UserSignedupPayload\\"}); @@ -83,37 +90,44 @@ exports[`JsonBinPack preset should work fine with JSON Schema draft 4 1`] = ` get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.email !== undefined) { - json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`; + json[\\"email\\"] = this.email; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"email\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): Root { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Root { const instance = new Root({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"email\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): Root { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Root.fromJson(obj as Record); + } public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"$schema\\":\\"https://json-schema.org/draft/2020-12/schema\\",\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"format\\":\\"email\\",\\"type\\":\\"string\\",\\"x-modelgen-inferred-name\\":\\"email\\"}},\\"x-modelgen-inferred-name\\":\\"root\\"}); @@ -147,37 +161,44 @@ exports[`JsonBinPack preset should work fine with JSON Schema draft 6 1`] = ` get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.email !== undefined) { - json += \`\\"email\\": \${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},\`; + json[\\"email\\"] = this.email; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"email\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): Root { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Root { const instance = new Root({} as any); if (obj[\\"email\\"] !== undefined) { instance.email = obj[\\"email\\"]; } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"email\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): Root { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Root.fromJson(obj as Record); + } public async jsonbinSerialize(): Promise{ const jsonData = JSON.parse(this.marshal()); const jsonbinpackEncodedSchema = await jsonbinpack.compileSchema({\\"$schema\\":\\"https://json-schema.org/draft/2020-12/schema\\",\\"type\\":\\"object\\",\\"properties\\":{\\"email\\":{\\"format\\":\\"email\\",\\"type\\":\\"string\\",\\"x-modelgen-inferred-name\\":\\"email\\"}},\\"x-modelgen-inferred-name\\":\\"root\\"}); diff --git a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap index ed71ec3795..2c35c44bde 100644 --- a/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap +++ b/test/generators/typescript/preset/__snapshots__/MarshallingPreset.spec.ts.snap @@ -43,43 +43,45 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.createdAt !== undefined) { - json += \`\\"createdAt\\": \${typeof this.createdAt === 'number' || typeof this.createdAt === 'boolean' ? this.createdAt : JSON.stringify(this.createdAt)},\`; + json[\\"createdAt\\"] = this.createdAt; } if(this.birthDate !== undefined) { - json += \`\\"birthDate\\": \${typeof this.birthDate === 'number' || typeof this.birthDate === 'boolean' ? this.birthDate : JSON.stringify(this.birthDate)},\`; + json[\\"birthDate\\"] = this.birthDate; } if(this.meetingTime !== undefined) { - json += \`\\"meetingTime\\": \${typeof this.meetingTime === 'number' || typeof this.meetingTime === 'boolean' ? this.meetingTime : JSON.stringify(this.meetingTime)},\`; + json[\\"meetingTime\\"] = this.meetingTime; } if(this.regularString !== undefined) { - json += \`\\"regularString\\": \${typeof this.regularString === 'number' || typeof this.regularString === 'boolean' ? this.regularString : JSON.stringify(this.regularString)},\`; + json[\\"regularString\\"] = this.regularString; } if(this.optionalDate !== undefined) { - json += \`\\"optionalDate\\": \${typeof this.optionalDate === 'number' || typeof this.optionalDate === 'boolean' ? this.optionalDate : JSON.stringify(this.optionalDate)},\`; + json[\\"optionalDate\\"] = this.optionalDate; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"createdAt\\",\\"birthDate\\",\\"meetingTime\\",\\"regularString\\",\\"optionalDate\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): DateTest { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): DateTest { const instance = new DateTest({} as any); if (obj[\\"createdAt\\"] !== undefined) { - instance.createdAt = obj[\\"createdAt\\"] == null ? null : new Date(obj[\\"createdAt\\"]); + instance.createdAt = obj[\\"createdAt\\"] == null ? null : new Date(obj[\\"createdAt\\"] as string); } if (obj[\\"birthDate\\"] !== undefined) { - instance.birthDate = obj[\\"birthDate\\"] == null ? undefined : new Date(obj[\\"birthDate\\"]); + instance.birthDate = obj[\\"birthDate\\"] == null ? undefined : new Date(obj[\\"birthDate\\"] as string); } if (obj[\\"meetingTime\\"] !== undefined) { instance.meetingTime = obj[\\"meetingTime\\"]; @@ -88,16 +90,21 @@ exports[`Marshalling preset date unmarshal should convert date-formatted string instance.regularString = obj[\\"regularString\\"]; } if (obj[\\"optionalDate\\"] !== undefined) { - instance.optionalDate = obj[\\"optionalDate\\"] == null ? undefined : new Date(obj[\\"optionalDate\\"]); + instance.optionalDate = obj[\\"optionalDate\\"] == null ? undefined : new Date(obj[\\"optionalDate\\"] as string); } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"createdAt\\",\\"birthDate\\",\\"meetingTime\\",\\"regularString\\",\\"optionalDate\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): DateTest { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return DateTest.fromJson(obj as Record); + } }" `; @@ -138,55 +145,62 @@ exports[`Marshalling preset nullable types (type: [null, string]) should generat get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.nullableString !== undefined) { - json += \`\\"nullableString\\": \${typeof this.nullableString === 'number' || typeof this.nullableString === 'boolean' ? this.nullableString : JSON.stringify(this.nullableString)},\`; + json[\\"nullableString\\"] = this.nullableString; } if(this.nullableDate !== undefined) { - json += \`\\"nullableDate\\": \${typeof this.nullableDate === 'number' || typeof this.nullableDate === 'boolean' ? this.nullableDate : JSON.stringify(this.nullableDate)},\`; + json[\\"nullableDate\\"] = this.nullableDate; } if(this.requiredNullableDate !== undefined) { - json += \`\\"requiredNullableDate\\": \${typeof this.requiredNullableDate === 'number' || typeof this.requiredNullableDate === 'boolean' ? this.requiredNullableDate : JSON.stringify(this.requiredNullableDate)},\`; + json[\\"requiredNullableDate\\"] = this.requiredNullableDate; } if(this.requiredDate !== undefined) { - json += \`\\"requiredDate\\": \${typeof this.requiredDate === 'number' || typeof this.requiredDate === 'boolean' ? this.requiredDate : JSON.stringify(this.requiredDate)},\`; + json[\\"requiredDate\\"] = this.requiredDate; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"nullableString\\",\\"nullableDate\\",\\"requiredNullableDate\\",\\"requiredDate\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): NullableTest { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): NullableTest { const instance = new NullableTest({} as any); if (obj[\\"nullableString\\"] !== undefined) { instance.nullableString = obj[\\"nullableString\\"]; } if (obj[\\"nullableDate\\"] !== undefined) { - instance.nullableDate = obj[\\"nullableDate\\"] == null ? undefined : new Date(obj[\\"nullableDate\\"]); + instance.nullableDate = obj[\\"nullableDate\\"] == null ? undefined : new Date(obj[\\"nullableDate\\"] as string); } if (obj[\\"requiredNullableDate\\"] !== undefined) { - instance.requiredNullableDate = obj[\\"requiredNullableDate\\"] == null ? null : new Date(obj[\\"requiredNullableDate\\"]); + instance.requiredNullableDate = obj[\\"requiredNullableDate\\"] == null ? null : new Date(obj[\\"requiredNullableDate\\"] as string); } if (obj[\\"requiredDate\\"] !== undefined) { - instance.requiredDate = obj[\\"requiredDate\\"] == null ? null : new Date(obj[\\"requiredDate\\"]); + instance.requiredDate = obj[\\"requiredDate\\"] == null ? null : new Date(obj[\\"requiredDate\\"] as string); } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"nullableString\\",\\"nullableDate\\",\\"requiredNullableDate\\",\\"requiredDate\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): NullableTest { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return NullableTest.fromJson(obj as Record); + } }" `; @@ -260,86 +274,65 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.stringProp !== undefined) { - json += \`\\"string prop\\": \${typeof this.stringProp === 'number' || typeof this.stringProp === 'boolean' ? this.stringProp : JSON.stringify(this.stringProp)},\`; + json[\\"string prop\\"] = this.stringProp; } if(this.enumProp !== undefined) { - json += \`\\"enumProp\\": \${typeof this.enumProp === 'number' || typeof this.enumProp === 'boolean' ? this.enumProp : JSON.stringify(this.enumProp)},\`; + json[\\"enumProp\\"] = this.enumProp; } if(this.numberProp !== undefined) { - json += \`\\"numberProp\\": \${typeof this.numberProp === 'number' || typeof this.numberProp === 'boolean' ? this.numberProp : JSON.stringify(this.numberProp)},\`; + json[\\"numberProp\\"] = this.numberProp; } if(this.nestedObject !== undefined) { - json += \`\\"nestedObject\\": \${this.nestedObject && typeof this.nestedObject === 'object' && 'marshal' in this.nestedObject && typeof this.nestedObject.marshal === 'function' ? this.nestedObject.marshal() : JSON.stringify(this.nestedObject)},\`; + json[\\"nestedObject\\"] = this.nestedObject && typeof this.nestedObject === 'object' && 'toJson' in this.nestedObject && typeof this.nestedObject.toJson === 'function' ? this.nestedObject.toJson() : this.nestedObject; } if(this.unionTest !== undefined) { - if(this.unionTest && typeof this.unionTest === 'object' && 'marshal' in this.unionTest && typeof this.unionTest.marshal === 'function') { - json += \`\\"unionTest\\": \${this.unionTest.marshal()},\`; - } else { - json += \`\\"unionTest\\": \${typeof this.unionTest === 'number' || typeof this.unionTest === 'boolean' ? this.unionTest : JSON.stringify(this.unionTest)},\`; - } + if(this.unionTest && typeof this.unionTest === 'object' && 'toJson' in this.unionTest && typeof this.unionTest.toJson === 'function') { + json[\\"unionTest\\"] = this.unionTest.toJson(); + } else { + json[\\"unionTest\\"] = this.unionTest; + } } if(this.unionArrayTest !== undefined) { - const unionArrayTestJsonValues: any[] = []; - for (const unionItem of this.unionArrayTest) { - if(unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function') { - unionArrayTestJsonValues.push(unionItem.marshal()); - } else { - unionArrayTestJsonValues.push(typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)) - } - } - json += \`\\"unionArrayTest\\": [\${unionArrayTestJsonValues.join(',')}],\`; + json[\\"unionArrayTest\\"] = this.unionArrayTest.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + ); } if(this.arrayTest !== undefined) { - let arrayTestJsonValues: any[] = []; - for (const unionItem of this.arrayTest) { - arrayTestJsonValues.push(\`\${unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function' ? unionItem.marshal() : JSON.stringify(unionItem)}\`); - } - json += \`\\"arrayTest\\": [\${arrayTestJsonValues.join(',')}],\`; + json[\\"arrayTest\\"] = this.arrayTest.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + ); } if(this.primitiveArrayTest !== undefined) { - let primitiveArrayTestJsonValues: any[] = []; - for (const unionItem of this.primitiveArrayTest) { - primitiveArrayTestJsonValues.push(\`\${typeof unionItem === 'number' || typeof unionItem === 'boolean' ? unionItem : JSON.stringify(unionItem)}\`); - } - json += \`\\"primitiveArrayTest\\": [\${primitiveArrayTestJsonValues.join(',')}],\`; + json[\\"primitiveArrayTest\\"] = this.primitiveArrayTest; } if(this.tupleTest !== undefined) { - const serializedTuple: any[] = []; - if(this.tupleTest[0]) { - serializedTuple[0] = \`\${this.tupleTest[0] && typeof this.tupleTest[0] === 'object' && 'marshal' in this.tupleTest[0] && typeof this.tupleTest[0].marshal === 'function' ? this.tupleTest[0].marshal() : JSON.stringify(this.tupleTest[0])}\` - } else { - serializedTuple[0] = null; - } - if(this.tupleTest[1]) { - serializedTuple[1] = \`\${typeof this.tupleTest[1] === 'number' || typeof this.tupleTest[1] === 'boolean' ? this.tupleTest[1] : JSON.stringify(this.tupleTest[1])}\` - } else { - serializedTuple[1] = null; - } - json += \`\\"tupleTest\\": [\${serializedTuple.join(',')}],\`; + json[\\"tupleTest\\"] = [this.tupleTest[0] !== undefined ? this.tupleTest[0] && typeof this.tupleTest[0] === 'object' && 'toJson' in this.tupleTest[0] && typeof this.tupleTest[0].toJson === 'function' ? this.tupleTest[0].toJson() : this.tupleTest[0] : null, this.tupleTest[1] !== undefined ? this.tupleTest[1] : null]; } if(this.constTest !== undefined) { - json += \`\\"constTest\\": \${typeof this.constTest === 'number' || typeof this.constTest === 'boolean' ? this.constTest : JSON.stringify(this.constTest)},\`; + json[\\"constTest\\"] = this.constTest; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"string prop\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"unionTest\\",\\"unionArrayTest\\",\\"arrayTest\\",\\"primitiveArrayTest\\",\\"tupleTest\\",\\"constTest\\",\\"additionalProperties\\"].includes(String(key))) continue; - if(value && typeof value === 'object' && 'marshal' in value && typeof value.marshal === 'function') { - json += \`\\"\${key}\\": \${value.marshal()},\`; - } else { - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; - } + json[key] = value && typeof value === 'object' && 'toJson' in value && typeof value.toJson === 'function' ? value.toJson() : value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): Test { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Test { const instance = new Test({} as any); if (obj[\\"string prop\\"] !== undefined) { @@ -352,7 +345,7 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` instance.numberProp = obj[\\"numberProp\\"]; } if (obj[\\"nestedObject\\"] !== undefined) { - instance.nestedObject = NestedTest.unmarshal(obj[\\"nestedObject\\"]); + instance.nestedObject = NestedTest.fromJson(obj[\\"nestedObject\\"] as Record); } if (obj[\\"unionTest\\"] !== undefined) { instance.unionTest = obj[\\"unionTest\\"]; @@ -363,7 +356,7 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` if (obj[\\"arrayTest\\"] !== undefined) { instance.arrayTest = obj[\\"arrayTest\\"] == null ? undefined - : obj[\\"arrayTest\\"].map((item: any) => NestedTest.unmarshal(item)); + : (obj[\\"arrayTest\\"] as Record[]).map((item: Record) => NestedTest.fromJson(item)); } if (obj[\\"primitiveArrayTest\\"] !== undefined) { instance.primitiveArrayTest = obj[\\"primitiveArrayTest\\"]; @@ -372,14 +365,19 @@ exports[`Marshalling preset should render un/marshal code 1`] = ` instance.tupleTest = obj[\\"tupleTest\\"]; } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"string prop\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"unionTest\\",\\"unionArrayTest\\",\\"arrayTest\\",\\"primitiveArrayTest\\",\\"tupleTest\\",\\"constTest\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): Test { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Test.fromJson(obj as Record); + } }" `; @@ -411,36 +409,220 @@ exports[`Marshalling preset should render un/marshal code 3`] = ` get additionalProperties(): Map | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.stringProp !== undefined) { - json += \`\\"stringProp\\": \${typeof this.stringProp === 'number' || typeof this.stringProp === 'boolean' ? this.stringProp : JSON.stringify(this.stringProp)},\`; + json[\\"stringProp\\"] = this.stringProp; } - if(this.additionalProperties !== undefined) { + if(this.additionalProperties !== undefined) { for (const [key, value] of this.additionalProperties.entries()) { //Only unwrap those that are not already a property in the JSON object if([\\"stringProp\\",\\"additionalProperties\\"].includes(String(key))) continue; - json += \`\\"\${key}\\": \${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},\`; + json[key] = value; } } - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; + return json; } - public static unmarshal(json: string | object): NestedTest { - const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): NestedTest { const instance = new NestedTest({} as any); if (obj[\\"stringProp\\"] !== undefined) { instance.stringProp = obj[\\"stringProp\\"]; } - + instance.additionalProperties = new Map(); const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"stringProp\\",\\"additionalProperties\\"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties.set(key, value); } return instance; } + + public static unmarshal(json: string | object): NestedTest { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return NestedTest.fromJson(obj as Record); + } +}" +`; + +exports[`Marshalling preset toJson/fromJson methods should render complete toJson/fromJson/marshal/unmarshal methods 1`] = ` +"class Test { + private _stringProp: string; + private _enumProp?: EnumTest; + private _numberProp?: number; + private _nestedObject?: NestedTest; + private _unionTest?: NestedTest | string; + private _unionArrayTest?: (NestedTest | string)[]; + private _arrayTest?: NestedTest[]; + private _primitiveArrayTest?: string[]; + private _tupleTest?: [NestedTest, string]; + private _constTest?: 'TEST' = 'TEST'; + private _additionalProperties?: Map; + + constructor(input: { + stringProp: string, + enumProp?: EnumTest, + numberProp?: number, + nestedObject?: NestedTest, + unionTest?: NestedTest | string, + unionArrayTest?: (NestedTest | string)[], + arrayTest?: NestedTest[], + primitiveArrayTest?: string[], + tupleTest?: [NestedTest, string], + additionalProperties?: Map, + }) { + this._stringProp = input.stringProp; + this._enumProp = input.enumProp; + this._numberProp = input.numberProp; + this._nestedObject = input.nestedObject; + this._unionTest = input.unionTest; + this._unionArrayTest = input.unionArrayTest; + this._arrayTest = input.arrayTest; + this._primitiveArrayTest = input.primitiveArrayTest; + this._tupleTest = input.tupleTest; + this._additionalProperties = input.additionalProperties; + } + + get stringProp(): string { return this._stringProp; } + set stringProp(stringProp: string) { this._stringProp = stringProp; } + + get enumProp(): EnumTest | undefined { return this._enumProp; } + set enumProp(enumProp: EnumTest | undefined) { this._enumProp = enumProp; } + + get numberProp(): number | undefined { return this._numberProp; } + set numberProp(numberProp: number | undefined) { this._numberProp = numberProp; } + + get nestedObject(): NestedTest | undefined { return this._nestedObject; } + set nestedObject(nestedObject: NestedTest | undefined) { this._nestedObject = nestedObject; } + + get unionTest(): NestedTest | string | undefined { return this._unionTest; } + set unionTest(unionTest: NestedTest | string | undefined) { this._unionTest = unionTest; } + + get unionArrayTest(): (NestedTest | string)[] | undefined { return this._unionArrayTest; } + set unionArrayTest(unionArrayTest: (NestedTest | string)[] | undefined) { this._unionArrayTest = unionArrayTest; } + + get arrayTest(): NestedTest[] | undefined { return this._arrayTest; } + set arrayTest(arrayTest: NestedTest[] | undefined) { this._arrayTest = arrayTest; } + + get primitiveArrayTest(): string[] | undefined { return this._primitiveArrayTest; } + set primitiveArrayTest(primitiveArrayTest: string[] | undefined) { this._primitiveArrayTest = primitiveArrayTest; } + + get tupleTest(): [NestedTest, string] | undefined { return this._tupleTest; } + set tupleTest(tupleTest: [NestedTest, string] | undefined) { this._tupleTest = tupleTest; } + + get constTest(): 'TEST' | undefined { return this._constTest; } + + get additionalProperties(): Map | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } + + public toJson(): Record { + const json: Record = {}; + if(this.stringProp !== undefined) { + json[\\"string prop\\"] = this.stringProp; + } + if(this.enumProp !== undefined) { + json[\\"enumProp\\"] = this.enumProp; + } + if(this.numberProp !== undefined) { + json[\\"numberProp\\"] = this.numberProp; + } + if(this.nestedObject !== undefined) { + json[\\"nestedObject\\"] = this.nestedObject && typeof this.nestedObject === 'object' && 'toJson' in this.nestedObject && typeof this.nestedObject.toJson === 'function' ? this.nestedObject.toJson() : this.nestedObject; + } + if(this.unionTest !== undefined) { + if(this.unionTest && typeof this.unionTest === 'object' && 'toJson' in this.unionTest && typeof this.unionTest.toJson === 'function') { + json[\\"unionTest\\"] = this.unionTest.toJson(); + } else { + json[\\"unionTest\\"] = this.unionTest; + } + } + if(this.unionArrayTest !== undefined) { + json[\\"unionArrayTest\\"] = this.unionArrayTest.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + ); + } + if(this.arrayTest !== undefined) { + json[\\"arrayTest\\"] = this.arrayTest.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + ); + } + if(this.primitiveArrayTest !== undefined) { + json[\\"primitiveArrayTest\\"] = this.primitiveArrayTest; + } + if(this.tupleTest !== undefined) { + json[\\"tupleTest\\"] = [this.tupleTest[0] !== undefined ? this.tupleTest[0] && typeof this.tupleTest[0] === 'object' && 'toJson' in this.tupleTest[0] && typeof this.tupleTest[0].toJson === 'function' ? this.tupleTest[0].toJson() : this.tupleTest[0] : null, this.tupleTest[1] !== undefined ? this.tupleTest[1] : null]; + } + if(this.constTest !== undefined) { + json[\\"constTest\\"] = this.constTest; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if([\\"string prop\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"unionTest\\",\\"unionArrayTest\\",\\"arrayTest\\",\\"primitiveArrayTest\\",\\"tupleTest\\",\\"constTest\\",\\"additionalProperties\\"].includes(String(key))) continue; + json[key] = value && typeof value === 'object' && 'toJson' in value && typeof value.toJson === 'function' ? value.toJson() : value; + } + } + return json; + } + + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): Test { + const instance = new Test({} as any); + + if (obj[\\"string prop\\"] !== undefined) { + instance.stringProp = obj[\\"string prop\\"]; + } + if (obj[\\"enumProp\\"] !== undefined) { + instance.enumProp = obj[\\"enumProp\\"]; + } + if (obj[\\"numberProp\\"] !== undefined) { + instance.numberProp = obj[\\"numberProp\\"]; + } + if (obj[\\"nestedObject\\"] !== undefined) { + instance.nestedObject = NestedTest.fromJson(obj[\\"nestedObject\\"] as Record); + } + if (obj[\\"unionTest\\"] !== undefined) { + instance.unionTest = obj[\\"unionTest\\"]; + } + if (obj[\\"unionArrayTest\\"] !== undefined) { + instance.unionArrayTest = obj[\\"unionArrayTest\\"]; + } + if (obj[\\"arrayTest\\"] !== undefined) { + instance.arrayTest = obj[\\"arrayTest\\"] == null + ? undefined + : (obj[\\"arrayTest\\"] as Record[]).map((item: Record) => NestedTest.fromJson(item)); + } + if (obj[\\"primitiveArrayTest\\"] !== undefined) { + instance.primitiveArrayTest = obj[\\"primitiveArrayTest\\"]; + } + if (obj[\\"tupleTest\\"] !== undefined) { + instance.tupleTest = obj[\\"tupleTest\\"]; + } + + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return ![\\"string prop\\",\\"enumProp\\",\\"numberProp\\",\\"nestedObject\\",\\"unionTest\\",\\"unionArrayTest\\",\\"arrayTest\\",\\"primitiveArrayTest\\",\\"tupleTest\\",\\"constTest\\",\\"additionalProperties\\"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value); + } + return instance; + } + + public static unmarshal(json: string | object): Test { + const obj = typeof json === \\"object\\" ? json : JSON.parse(json); + return Test.fromJson(obj as Record); + } }" `; diff --git a/test/runtime/runtime-typescript/src/marshalling/ObjectType.ts b/test/runtime/runtime-typescript/src/marshalling/ObjectType.ts new file mode 100644 index 0000000000..07df6648af --- /dev/null +++ b/test/runtime/runtime-typescript/src/marshalling/ObjectType.ts @@ -0,0 +1,59 @@ + +class ObjectType { + private _test?: string; + private _additionalProperties?: Map; + + constructor(input: { + test?: string, + additionalProperties?: Map, + }) { + this._test = input.test; + this._additionalProperties = input.additionalProperties; + } + + get test(): string | undefined { return this._test; } + set test(test: string | undefined) { this._test = test; } + + get additionalProperties(): Map | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } + + public toJson(): Record { + const json: Record = {}; + if(this.test !== undefined) { + json["test"] = this.test; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["test","additionalProperties"].includes(String(key))) continue; + json[key] = value; + } + } + return json; + } + + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): ObjectType { + const instance = new ObjectType({} as any); + + if (obj["test"] !== undefined) { + instance.test = obj["test"] as string; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["test","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + + public static unmarshal(json: string | object): ObjectType { + const obj = typeof json === "object" ? json : JSON.parse(json); + return ObjectType.fromJson(obj as Record); + } +} +export { ObjectType }; \ No newline at end of file diff --git a/test/runtime/runtime-typescript/src/marshalling/TestObject.ts b/test/runtime/runtime-typescript/src/marshalling/TestObject.ts new file mode 100644 index 0000000000..365a7ae6ed --- /dev/null +++ b/test/runtime/runtime-typescript/src/marshalling/TestObject.ts @@ -0,0 +1,204 @@ +import {ObjectType} from './ObjectType'; +import {EnumType} from './EnumType'; +class TestObject { + private _stringType: string; + private _createdAt?: Date; + private _numberType?: number; + private _booleanType: boolean; + private _unionType?: string | number | boolean; + private _arrayType?: (string | number)[]; + private _tupleType?: [string, number]; + private _objectType?: ObjectType; + private _dictionaryType?: Map; + private _enumType?: EnumType; + private _nullableString?: string | null; + private _nullableDate?: Date | null; + private _requiredNullableDate: Date | null; + private _additionalProperties?: Map; + + constructor(input: { + stringType: string, + createdAt?: Date, + numberType?: number, + booleanType: boolean, + unionType?: string | number | boolean, + arrayType?: (string | number)[], + tupleType?: [string, number], + objectType?: ObjectType, + dictionaryType?: Map, + enumType?: EnumType, + nullableString?: string | null, + nullableDate?: Date | null, + requiredNullableDate: Date | null, + additionalProperties?: Map, + }) { + this._stringType = input.stringType; + this._createdAt = input.createdAt; + this._numberType = input.numberType; + this._booleanType = input.booleanType; + this._unionType = input.unionType; + this._arrayType = input.arrayType; + this._tupleType = input.tupleType; + this._objectType = input.objectType; + this._dictionaryType = input.dictionaryType; + this._enumType = input.enumType; + this._nullableString = input.nullableString; + this._nullableDate = input.nullableDate; + this._requiredNullableDate = input.requiredNullableDate; + this._additionalProperties = input.additionalProperties; + } + + get stringType(): string { return this._stringType; } + set stringType(stringType: string) { this._stringType = stringType; } + + get createdAt(): Date | undefined { return this._createdAt; } + set createdAt(createdAt: Date | undefined) { this._createdAt = createdAt; } + + get numberType(): number | undefined { return this._numberType; } + set numberType(numberType: number | undefined) { this._numberType = numberType; } + + get booleanType(): boolean { return this._booleanType; } + set booleanType(booleanType: boolean) { this._booleanType = booleanType; } + + get unionType(): string | number | boolean | undefined { return this._unionType; } + set unionType(unionType: string | number | boolean | undefined) { this._unionType = unionType; } + + get arrayType(): (string | number)[] | undefined { return this._arrayType; } + set arrayType(arrayType: (string | number)[] | undefined) { this._arrayType = arrayType; } + + get tupleType(): [string, number] | undefined { return this._tupleType; } + set tupleType(tupleType: [string, number] | undefined) { this._tupleType = tupleType; } + + get objectType(): ObjectType | undefined { return this._objectType; } + set objectType(objectType: ObjectType | undefined) { this._objectType = objectType; } + + get dictionaryType(): Map | undefined { return this._dictionaryType; } + set dictionaryType(dictionaryType: Map | undefined) { this._dictionaryType = dictionaryType; } + + get enumType(): EnumType | undefined { return this._enumType; } + set enumType(enumType: EnumType | undefined) { this._enumType = enumType; } + + get nullableString(): string | null | undefined { return this._nullableString; } + set nullableString(nullableString: string | null | undefined) { this._nullableString = nullableString; } + + get nullableDate(): Date | null | undefined { return this._nullableDate; } + set nullableDate(nullableDate: Date | null | undefined) { this._nullableDate = nullableDate; } + + get requiredNullableDate(): Date | null { return this._requiredNullableDate; } + set requiredNullableDate(requiredNullableDate: Date | null) { this._requiredNullableDate = requiredNullableDate; } + + get additionalProperties(): Map | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } + + public toJson(): Record { + const json: Record = {}; + if(this.stringType !== undefined) { + json["string_type"] = this.stringType; + } + if(this.createdAt !== undefined) { + json["createdAt"] = this.createdAt; + } + if(this.numberType !== undefined) { + json["number_type"] = this.numberType; + } + if(this.booleanType !== undefined) { + json["boolean_type"] = this.booleanType; + } + if(this.unionType !== undefined) { + json["union_type"] = this.unionType; + } + if(this.arrayType !== undefined) { + json["array_type"] = this.arrayType; + } + if(this.tupleType !== undefined) { + json["tuple_type"] = [this.tupleType[0] !== undefined ? this.tupleType[0] : null, this.tupleType[1] !== undefined ? this.tupleType[1] : null]; + } + if(this.objectType !== undefined) { + json["object_type"] = this.objectType && typeof this.objectType === 'object' && 'toJson' in this.objectType && typeof this.objectType.toJson === 'function' ? this.objectType.toJson() : this.objectType; + } + if(this.dictionaryType !== undefined) { + json["dictionary_type"] = this.dictionaryType; + } + if(this.enumType !== undefined) { + json["enum_type"] = this.enumType; + } + if(this.nullableString !== undefined) { + json["nullable_string"] = this.nullableString; + } + if(this.nullableDate !== undefined) { + json["nullable_date"] = this.nullableDate; + } + if(this.requiredNullableDate !== undefined) { + json["required_nullable_date"] = this.requiredNullableDate; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //Only unwrap those that are not already a property in the JSON object + if(["string_type","createdAt","number_type","boolean_type","union_type","array_type","tuple_type","object_type","dictionary_type","enum_type","nullable_string","nullable_date","required_nullable_date","additionalProperties"].includes(String(key))) continue; + json[key] = value; + } + } + return json; + } + + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): TestObject { + const instance = new TestObject({} as any); + + if (obj["string_type"] !== undefined) { + instance.stringType = obj["string_type"] as string; + } + if (obj["createdAt"] !== undefined) { + instance.createdAt = obj["createdAt"] == null ? undefined : new Date(obj["createdAt"] as string); + } + if (obj["number_type"] !== undefined) { + instance.numberType = obj["number_type"] as number; + } + if (obj["boolean_type"] !== undefined) { + instance.booleanType = obj["boolean_type"] as boolean; + } + if (obj["union_type"] !== undefined) { + instance.unionType = obj["union_type"] as string | number | boolean; + } + if (obj["array_type"] !== undefined) { + instance.arrayType = obj["array_type"] as (string | number)[]; + } + if (obj["tuple_type"] !== undefined) { + instance.tupleType = obj["tuple_type"] as [string, number]; + } + if (obj["object_type"] !== undefined) { + instance.objectType = ObjectType.fromJson(obj["object_type"] as Record); + } + if (obj["dictionary_type"] !== undefined) { + instance.dictionaryType = obj["dictionary_type"] as Map; + } + if (obj["enum_type"] !== undefined) { + instance.enumType = obj["enum_type"] as EnumType; + } + if (obj["nullable_string"] !== undefined) { + instance.nullableString = obj["nullable_string"] as string | null; + } + if (obj["nullable_date"] !== undefined) { + instance.nullableDate = obj["nullable_date"] == null ? undefined : new Date(obj["nullable_date"] as string); + } + if (obj["required_nullable_date"] !== undefined) { + instance.requiredNullableDate = obj["required_nullable_date"] == null ? null : new Date(obj["required_nullable_date"] as string); + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["string_type","createdAt","number_type","boolean_type","union_type","array_type","tuple_type","object_type","dictionary_type","enum_type","nullable_string","nullable_date","required_nullable_date","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + + public static unmarshal(json: string | object): TestObject { + const obj = typeof json === "object" ? json : JSON.parse(json); + return TestObject.fromJson(obj as Record); + } +} +export { TestObject }; \ No newline at end of file diff --git a/test/runtime/runtime-typescript/test/Marshalling.spec.ts b/test/runtime/runtime-typescript/test/Marshalling.spec.ts index 631da4ec59..c17dce6041 100644 --- a/test/runtime/runtime-typescript/test/Marshalling.spec.ts +++ b/test/runtime/runtime-typescript/test/Marshalling.spec.ts @@ -22,11 +22,67 @@ describe('Marshalling', () => { unionType: 'test', requiredNullableDate: new Date('2023-06-15T12:00:00Z'), }); + + test('toJson should return a plain object', () => { + const json = testObject.toJson(); + expect(typeof json).toBe('object'); + expect(json).not.toBeInstanceOf(String); + expect(json["string_type"]).toBe('test'); + expect(json["number_type"]).toBe(1); + expect(json["boolean_type"]).toBe(true); + }); + + test('toJson should handle nested objects by calling their toJson', () => { + const json = testObject.toJson(); + // Nested objectType should be converted to plain object + expect(json["object_type"]).toEqual({ test: 'test' }); + }); + + test('fromJson should accept a plain object and return an instance', () => { + const json = testObject.toJson(); + const instance = TestObject.fromJson(json); + expect(instance).toBeInstanceOf(TestObject); + expect(instance.stringType).toBe('test'); + expect(instance.numberType).toBe(1); + expect(instance.booleanType).toBe(true); + }); + + test('fromJson should handle nested objects by calling their fromJson', () => { + const json = testObject.toJson(); + const instance = TestObject.fromJson(json); + expect(instance.objectType).toBeInstanceOf(ObjectType); + expect(instance.objectType?.test).toBe('test'); + }); + + test('round-trip: fromJson(toJson()) produces equivalent instance', () => { + const json = testObject.toJson(); + const instance = TestObject.fromJson(json); + // Compare serialized output to verify equivalence + expect(instance.marshal()).toEqual(testObject.marshal()); + }); + + test('marshal should use JSON.stringify(toJson())', () => { + const marshalled = testObject.marshal(); + const toJsonResult = testObject.toJson(); + expect(marshalled).toEqual(JSON.stringify(toJsonResult)); + }); + + test('unmarshal(object) should call fromJson directly', () => { + const json = testObject.toJson(); + // unmarshal should accept object directly (backward compatibility) + const instance = TestObject.unmarshal(json as any); + expect(instance).toBeInstanceOf(TestObject); + expect(instance.stringType).toBe('test'); + }); + test('be able to serialize model', () => { const serialized = testObject.marshal(); - expect(serialized).toEqual( - "{\"string_type\": \"test\",\"createdAt\": \"2023-01-01T10:00:00.000Z\",\"number_type\": 1,\"boolean_type\": true,\"union_type\": \"test\",\"array_type\": [1,\"test\"],\"tuple_type\": [\"test\",1],\"object_type\": {\"test\": \"test\"},\"dictionary_type\": {},\"enum_type\": \"{\\\"test\\\":2}\",\"required_nullable_date\": \"2023-06-15T12:00:00.000Z\",\"test\": \"test\"}" -);}); + // Note: marshal now uses JSON.stringify(toJson()) which produces standard JSON + const parsed = JSON.parse(serialized); + expect(parsed["string_type"]).toBe('test'); + expect(parsed["number_type"]).toBe(1); + expect(parsed["boolean_type"]).toBe(true); + }); test('be able to serialize model and turning it back to a model with the same values', () => { const serialized = testObject.marshal(); const newAddress = TestObject.unmarshal(serialized);