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 @@ -1382,6 +1382,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,28 @@ 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;
{{/hasSanitizedName}}
{{^hasSanitizedName}}
if (!('{{name}}' in value) || value['{{name}}'] === undefined) return false;
{{/hasSanitizedName}}
{{#isEnum}}
{{#allowableValues}}
{{#values}}
{{#-first}}
{{#-last}}
{{#hasSanitizedName}}
if (value['{{name}}'] !== '{{.}}' && value['{{baseName}}'] !== '{{.}}') return false;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 8, 2026

Choose a reason for hiding this comment

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

P2: Enum discriminator check always compares against a quoted literal, which breaks numeric/boolean singleton enums by forcing string comparison in instanceOf guards.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache, line 40:

<comment>Enum discriminator check always compares against a quoted literal, which breaks numeric/boolean singleton enums by forcing string comparison in instanceOf guards.</comment>

<file context>
@@ -25,7 +25,28 @@ import { type {{modelName}}, {{modelName}}FromJSONTyped, {{modelName}}ToJSON, {{
+    {{#-first}}
+    {{#-last}}
+    {{#hasSanitizedName}}
+    if (value['{{name}}'] !== '{{.}}' && value['{{baseName}}'] !== '{{.}}') return false;
+    {{/hasSanitizedName}}
+    {{^hasSanitizedName}}
</file context>
Fix with Cubic

{{/hasSanitizedName}}
{{^hasSanitizedName}}
if (value['{{name}}'] !== '{{.}}') return false;
{{/hasSanitizedName}}
{{/-last}}
{{/-first}}
{{/values}}
{{/allowableValues}}
{{/isEnum}}
{{/required}}
{{/vars}}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,48 @@ 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");
}

@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,70 @@ components:
- "optionTwo"
type: string
required:
- discriminatorField
- discriminatorField
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,32 @@
apis/DefaultApi.ts
apis/index.ts
docs/DashedOptionOne.md
docs/DashedOptionTwo.md
docs/DefaultApi.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/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 @@ -16,16 +16,22 @@
import * as runtime from '../runtime';
import type {
TestArrayResponse,
TestDashedDiscriminatorResponse,
TestDiscriminatorResponse,
TestResponse,
TestSnakeCaseDiscriminatorResponse,
} from '../models/index';
import {
TestArrayResponseFromJSON,
TestArrayResponseToJSON,
TestDashedDiscriminatorResponseFromJSON,
TestDashedDiscriminatorResponseToJSON,
TestDiscriminatorResponseFromJSON,
TestDiscriminatorResponseToJSON,
TestResponseFromJSON,
TestResponseToJSON,
TestSnakeCaseDiscriminatorResponseFromJSON,
TestSnakeCaseDiscriminatorResponseToJSON,
} from '../models/index';

/**
Expand Down Expand Up @@ -103,6 +109,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 @@ -138,4 +179,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();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

# DashedOptionOne


## Properties

Name | Type
------------ | -------------
`discriminatorField` | string
`someProperty` | string

## Example

```typescript
import type { DashedOptionOne } from ''

// TODO: Update the object below with actual values
const example = {
"discriminatorField": null,
"someProperty": null,
} satisfies DashedOptionOne

console.log(example)

// Convert the instance to a JSON string
const exampleJSON: string = JSON.stringify(example)
console.log(exampleJSON)

// Parse the JSON string back to an object
const exampleParsed = JSON.parse(exampleJSON) as DashedOptionOne
console.log(exampleParsed)
```

[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md)


Loading