Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,7 @@ public ExtendedCodegenProperty(CodegenProperty cp) {
this.xmlName = cp.xmlName;
this.xmlNamespace = cp.xmlNamespace;
this.isXmlWrapped = cp.isXmlWrapped;
this.setHasSanitizedName(cp.getHasSanitizedName());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,30 @@ import { type {{modelName}}, {{modelName}}FromJSONTyped, {{modelName}}ToJSON, {{
export function instanceOf{{classname}}(value: object): value is {{classname}} {
{{#vars}}
{{#required}}
{{#hasSanitizedName}}
if ((!('{{name}}' in value) && !('{{baseName}}' in value)) || (value['{{name}}'] === undefined && value['{{baseName}}'] === undefined)) return false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldnt it be either name or baseName?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@macjohnny

instanceOf is called in both directions, so it needs to handle both casings:

  • Decoding in FromJSONTyped: called on raw JSON, where keys use baseName (e.g., discriminator-field)
  • Encoding in ToJSONTyped: called on the TS object, where keys use name (e.g., discriminatorField)

The {{#hasSanitizedName}} guard ensures we only emit the dual check when it's actually needed — for the common case where name === baseName, it falls back to the simpler single check.

I did consider making the function aware of direction but it felt too complex for not much gain, and would change the public instanceOf API.

{{/hasSanitizedName}}
{{^hasSanitizedName}}
if (!('{{name}}' in value) || value['{{name}}'] === undefined) return false;
{{/hasSanitizedName}}
{{#isEnum}}
{{#allowableValues}}
{{#values}}
{{#-first}}
{{#-last}}
{{#hasSanitizedName}}
{{#isString}}if (value['{{name}}'] !== '{{.}}' && value['{{baseName}}'] !== '{{.}}') return false;{{/isString}}
{{^isString}}if (value['{{name}}'] !== {{.}} && value['{{baseName}}'] !== {{.}}) return false;{{/isString}}
{{/hasSanitizedName}}
{{^hasSanitizedName}}
{{#isString}}if (value['{{name}}'] !== '{{.}}') return false;{{/isString}}
{{^isString}}if (value['{{name}}'] !== {{.}}) return false;{{/isString}}
{{/hasSanitizedName}}
{{/-last}}
{{/-first}}
{{/values}}
{{/allowableValues}}
{{/isEnum}}
{{/required}}
{{/vars}}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
switch (value['{{discriminator.propertyName}}']) {
{{#discriminator.mappedModels}}
case '{{mappingName}}':
return Object.assign({}, {{modelName}}ToJSON(value), { {{discriminator.propertyName}}: '{{mappingName}}' } as const);
return Object.assign({}, {{modelName}}ToJSON(value), { '{{discriminator.propertyBaseName}}': '{{mappingName}}' } as const);
{{/discriminator.mappedModels}}
default:
return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,66 @@ public void testOneOfModelsImportNonPrimitiveTypes() throws IOException {
TestUtils.assertFileContains(testResponse, "import type { OptionThree } from './OptionThree'");
}

@Test(description = "Verify instanceOf checks discriminator value for single-value enums")
public void testInstanceOfChecksDiscriminatorValue() throws IOException {
File output = generate(Collections.emptyMap(), "src/test/resources/3_0/typescript-fetch/oneOf.yaml");

// OptionOne should check discriminator value
Path optionOne = Paths.get(output + "/models/OptionOne.ts");
TestUtils.assertFileExists(optionOne);
TestUtils.assertFileContains(optionOne, "value['discriminatorField'] !== 'optionOne'");

// OptionTwo should check discriminator value
Path optionTwo = Paths.get(output + "/models/OptionTwo.ts");
TestUtils.assertFileExists(optionTwo);
TestUtils.assertFileContains(optionTwo, "value['discriminatorField'] !== 'optionTwo'");

// TestA should NOT have a value check (foo is a plain string, not a single-value enum)
Path testA = Paths.get(output + "/models/TestA.ts");
TestUtils.assertFileExists(testA);
TestUtils.assertFileNotContains(testA, "value['foo'] !==");

// SnakeOptionOne: discriminator_field (snake_case baseName) vs discriminatorField (camelCase name)
// instanceOf should check both casings for field presence and discriminator value
Path snakeOptionOne = Paths.get(output + "/models/SnakeOptionOne.ts");
TestUtils.assertFileExists(snakeOptionOne);
TestUtils.assertFileContains(snakeOptionOne, "'discriminatorField' in value");
TestUtils.assertFileContains(snakeOptionOne, "'discriminator_field' in value");
TestUtils.assertFileContains(snakeOptionOne, "value['discriminatorField'] !== 'snakeOptionOne'");
TestUtils.assertFileContains(snakeOptionOne, "value['discriminator_field'] !== 'snakeOptionOne'");
// Also verify the non-enum required field checks both casings
TestUtils.assertFileContains(snakeOptionOne, "'someProperty' in value");
TestUtils.assertFileContains(snakeOptionOne, "'some_property' in value");

// DashedOptionOne: discriminator-field (dashed baseName) vs discriminatorField (camelCase name)
Path dashedOptionOne = Paths.get(output + "/models/DashedOptionOne.ts");
TestUtils.assertFileExists(dashedOptionOne);
TestUtils.assertFileContains(dashedOptionOne, "'discriminatorField' in value");
TestUtils.assertFileContains(dashedOptionOne, "'discriminator-field' in value");
TestUtils.assertFileContains(dashedOptionOne, "value['discriminatorField'] !== 'dashedOptionOne'");
TestUtils.assertFileContains(dashedOptionOne, "value['discriminator-field'] !== 'dashedOptionOne'");
TestUtils.assertFileContains(dashedOptionOne, "'someProperty' in value");
TestUtils.assertFileContains(dashedOptionOne, "'some-property' in value");

// Numeric singleton enum: value check must NOT quote the literal
Path numericModel = Paths.get(output + "/models/NumericSingletonEnumModel.ts");
TestUtils.assertFileExists(numericModel);
TestUtils.assertFileContains(numericModel, "value['kind'] !== 42");
TestUtils.assertFileNotContains(numericModel, "value['kind'] !== '42'");

// ToJSONTyped of discriminated oneOf must emit the wire-format discriminator key
// (propertyBaseName), not the camelCase TS property name
Path dashedDiscriminatorResponse = Paths.get(output + "/models/TestDashedDiscriminatorResponse.ts");
TestUtils.assertFileExists(dashedDiscriminatorResponse);
TestUtils.assertFileContains(dashedDiscriminatorResponse, "{ 'discriminator-field': 'dashedOptionOne' }");
TestUtils.assertFileContains(dashedDiscriminatorResponse, "{ 'discriminator-field': 'dashedOptionTwo' }");

Path snakeDiscriminatorResponse = Paths.get(output + "/models/TestSnakeCaseDiscriminatorResponse.ts");
TestUtils.assertFileExists(snakeDiscriminatorResponse);
TestUtils.assertFileContains(snakeDiscriminatorResponse, "{ 'discriminator_field': 'snakeOptionOne' }");
TestUtils.assertFileContains(snakeDiscriminatorResponse, "{ 'discriminator_field': 'snakeOptionTwo' }");
}

@Test(description = "Verify validationAttributes works with withoutRuntimeChecks=true")
public void testValidationAttributesWithWithoutRuntimeChecks() throws IOException {
Map<String, Object> properties = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/TestDiscriminatorResponse'
/test-snake-case-discriminator:
get:
operationId: testSnakeCaseDiscriminator
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestSnakeCaseDiscriminatorResponse'
/test-dashed-discriminator:
get:
operationId: testDashedDiscriminator
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/TestDashedDiscriminatorResponse'
components:
schemas:
TestArrayResponse:
Expand Down Expand Up @@ -93,4 +113,79 @@ components:
- "optionTwo"
type: string
required:
- discriminatorField
- discriminatorField
NumericSingletonEnumModel:
type: object
properties:
kind:
type: integer
enum:
- 42
required:
- kind
TestSnakeCaseDiscriminatorResponse:
discriminator:
propertyName: discriminator_field
mapping:
snakeOptionOne: "#/components/schemas/SnakeOptionOne"
snakeOptionTwo: "#/components/schemas/SnakeOptionTwo"
oneOf:
- $ref: "#/components/schemas/SnakeOptionOne"
- $ref: "#/components/schemas/SnakeOptionTwo"
SnakeOptionOne:
type: object
properties:
discriminator_field:
enum:
- "snakeOptionOne"
type: string
some_property:
type: string
required:
- discriminator_field
- some_property
SnakeOptionTwo:
type: object
properties:
discriminator_field:
enum:
- "snakeOptionTwo"
type: string
some_property:
type: string
required:
- discriminator_field
- some_property
TestDashedDiscriminatorResponse:
discriminator:
propertyName: discriminator-field
mapping:
dashedOptionOne: "#/components/schemas/DashedOptionOne"
dashedOptionTwo: "#/components/schemas/DashedOptionTwo"
oneOf:
- $ref: "#/components/schemas/DashedOptionOne"
- $ref: "#/components/schemas/DashedOptionTwo"
DashedOptionOne:
type: object
properties:
discriminator-field:
enum:
- "dashedOptionOne"
type: string
some-property:
type: string
required:
- discriminator-field
- some-property
DashedOptionTwo:
type: object
properties:
discriminator-field:
enum:
- "dashedOptionTwo"
type: string
some-property:
type: string
required:
- discriminator-field
- some-property
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export type EnumTestEnumNumberEnum = typeof EnumTestEnumNumberEnum[keyof typeof
* Check if a given object implements the EnumTest interface.
*/
export function instanceOfEnumTest(value: object): value is EnumTest {
if (!('enumStringRequired' in value) || value['enumStringRequired'] === undefined) return false;
if ((!('enumStringRequired' in value) && !('enum_string_required' in value)) || (value['enumStringRequired'] === undefined && value['enum_string_required'] === undefined)) return false;
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export interface FormatTest {
*/
export function instanceOfFormatTest(value: object): value is FormatTest {
if (!('number' in value) || value['number'] === undefined) return false;
if (!('_byte' in value) || value['_byte'] === undefined) return false;
if ((!('_byte' in value) && !('byte' in value)) || (value['_byte'] === undefined && value['byte'] === undefined)) return false;
if (!('date' in value) || value['date'] === undefined) return false;
if (!('password' in value) || value['password'] === undefined) return false;
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,34 @@
apis/DefaultApi.ts
apis/index.ts
docs/DashedOptionOne.md
docs/DashedOptionTwo.md
docs/DefaultApi.md
docs/NumericSingletonEnumModel.md
docs/OptionOne.md
docs/OptionTwo.md
docs/SnakeOptionOne.md
docs/SnakeOptionTwo.md
docs/TestA.md
docs/TestArrayResponse.md
docs/TestB.md
docs/TestDashedDiscriminatorResponse.md
docs/TestDiscriminatorResponse.md
docs/TestResponse.md
docs/TestSnakeCaseDiscriminatorResponse.md
index.ts
models/DashedOptionOne.ts
models/DashedOptionTwo.ts
models/NumericSingletonEnumModel.ts
models/OptionOne.ts
models/OptionTwo.ts
models/SnakeOptionOne.ts
models/SnakeOptionTwo.ts
models/TestA.ts
models/TestArrayResponse.ts
models/TestB.ts
models/TestDashedDiscriminatorResponse.ts
models/TestDiscriminatorResponse.ts
models/TestResponse.ts
models/TestSnakeCaseDiscriminatorResponse.ts
models/index.ts
runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import {
TestArrayResponseFromJSON,
TestArrayResponseToJSON,
} from '../models/TestArrayResponse';
import {
type TestDashedDiscriminatorResponse,
TestDashedDiscriminatorResponseFromJSON,
TestDashedDiscriminatorResponseToJSON,
} from '../models/TestDashedDiscriminatorResponse';
import {
type TestDiscriminatorResponse,
TestDiscriminatorResponseFromJSON,
Expand All @@ -28,6 +33,11 @@ import {
TestResponseFromJSON,
TestResponseToJSON,
} from '../models/TestResponse';
import {
type TestSnakeCaseDiscriminatorResponse,
TestSnakeCaseDiscriminatorResponseFromJSON,
TestSnakeCaseDiscriminatorResponseToJSON,
} from '../models/TestSnakeCaseDiscriminatorResponse';

/**
*
Expand Down Expand Up @@ -104,6 +114,41 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value();
}

/**
* Creates request options for testDashedDiscriminator without sending the request
*/
async testDashedDiscriminatorRequestOpts(): Promise<runtime.RequestOpts> {
const queryParameters: any = {};

const headerParameters: runtime.HTTPHeaders = {};


let urlPath = `/test-dashed-discriminator`;

return {
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
};
}

/**
*/
async testDashedDiscriminatorRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<TestDashedDiscriminatorResponse>> {
const requestOptions = await this.testDashedDiscriminatorRequestOpts();
const response = await this.request(requestOptions, initOverrides);

return new runtime.JSONApiResponse(response, (jsonValue) => TestDashedDiscriminatorResponseFromJSON(jsonValue));
}

/**
*/
async testDashedDiscriminator(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<TestDashedDiscriminatorResponse> {
const response = await this.testDashedDiscriminatorRaw(initOverrides);
return await response.value();
}

/**
* Creates request options for testDiscriminator without sending the request
*/
Expand Down Expand Up @@ -139,4 +184,39 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value();
}

/**
* Creates request options for testSnakeCaseDiscriminator without sending the request
*/
async testSnakeCaseDiscriminatorRequestOpts(): Promise<runtime.RequestOpts> {
const queryParameters: any = {};

const headerParameters: runtime.HTTPHeaders = {};


let urlPath = `/test-snake-case-discriminator`;

return {
path: urlPath,
method: 'GET',
headers: headerParameters,
query: queryParameters,
};
}

/**
*/
async testSnakeCaseDiscriminatorRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<TestSnakeCaseDiscriminatorResponse>> {
const requestOptions = await this.testSnakeCaseDiscriminatorRequestOpts();
const response = await this.request(requestOptions, initOverrides);

return new runtime.JSONApiResponse(response, (jsonValue) => TestSnakeCaseDiscriminatorResponseFromJSON(jsonValue));
}

/**
*/
async testSnakeCaseDiscriminator(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<TestSnakeCaseDiscriminatorResponse> {
const response = await this.testSnakeCaseDiscriminatorRaw(initOverrides);
return await response.value();
}

}
Loading
Loading