Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0e6998f
Create OneOfInterface for OneOfModels
ksvirkou-hubspot Oct 8, 2025
451f488
Typescript OneOf: Find matching type if OneClass has no discriminator
ksvirkou-hubspot Oct 13, 2025
74351c7
Typescript: Test oneOf
ksvirkou-hubspot Oct 21, 2025
fe10060
cs fixes
ksvirkou-hubspot Oct 22, 2025
234643d
Update samples
ksvirkou-hubspot Oct 22, 2025
e03add7
Rewrite OneOfClass to standalone helper TypeMatcher
ksvirkou-hubspot Oct 24, 2025
49e768a
RM unused OneOfClasses
ksvirkou-hubspot Oct 24, 2025
97a775d
RM unused OneOfClasses
ksvirkou-hubspot Oct 24, 2025
eee422e
Improve oneOf type categorization in ExtendedCodegenModel
ksvirkou-hubspot Oct 27, 2025
81acbbc
Improve type safety and error reporting for oneOf schema resolution
ksvirkou-hubspot Oct 29, 2025
8e7a128
Restructured model.mustache
ksvirkou-hubspot Oct 29, 2025
de4e09e
Update instanceOfType metthod
ksvirkou-hubspot Oct 30, 2025
7da0c2a
Merge master
ksvirkou-hubspot Mar 30, 2026
ca525a9
Add oneOf tests for TypeScript generator to CI
ksvirkou-hubspot Mar 30, 2026
0bd35fc
Regenerate samples
ksvirkou-hubspot Mar 31, 2026
a117fcc
Regenerate samples
ksvirkou-hubspot Mar 31, 2026
c590359
Regenerate samples
ksvirkou-hubspot Mar 31, 2026
ded868c
Typescript test OneOf: added pow.xml
ksvirkou-hubspot Apr 3, 2026
c806d60
Merge pull request #3 from OpenAPITools/master
ksvirkou-hubspot Apr 3, 2026
1a4b46d
Typescript test OneOf: added tests to CI
ksvirkou-hubspot Apr 3, 2026
8fd54d2
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
6be8f0c
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
69954aa
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
23a2dd7
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
3bfe9cc
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
14d1edc
Typescript test OneOf: fix CI errors
ksvirkou-hubspot Apr 3, 2026
d4984c0
Typescript test OneOf: Added module to build tsconfig
ksvirkou-hubspot Apr 3, 2026
b04a5ed
Typescript test OneOf: added error to tests
ksvirkou-hubspot Apr 3, 2026
f8c65fb
rm error
ksvirkou-hubspot Apr 3, 2026
34c403a
Update tests
ksvirkou-hubspot Apr 7, 2026
3cd8558
Update tests
ksvirkou-hubspot Apr 7, 2026
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 @@ -174,6 +174,8 @@ public TypeScriptClientCodegen() {
// models
setModelPackage("models");
supportingFiles.add(new SupportingFile("model" + File.separator + "ObjectSerializer.mustache", "models", "ObjectSerializer.ts"));
supportingFiles.add(new SupportingFile("model" + File.separator + "TypeMatcher.mustache", "models", "TypeMatcher.ts"));

modelTemplateFiles.put("model" + File.separator + "model.mustache", ".ts");

// api
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,15 @@ export class ObjectSerializer {
// Check the discriminator
let discriminatorProperty = typeMap[expectedType].discriminator;
if (discriminatorProperty == null) {
return expectedType; // the type does not have a discriminator. use it.
if (this.hasFindMatchingTypeMethod(typeMap[expectedType])) {
const foundType = typeMap[expectedType].findMatchingType(data);
if (foundType == undefined) {
throw new Error("Unable to determine a unique type for the provided object: oneOf type resolution failed. The object does not match exactly one schema. Consider adding a discriminator or making schemas mutually exclusive.");
}

return foundType;
}
return expectedType; // the type does not have a discriminator and findMatchingType method. use it.
} else {
if (data[discriminatorProperty]) {
var discriminatorType = data[discriminatorProperty];
Expand All @@ -147,6 +155,13 @@ export class ObjectSerializer {
}
}

private static hasFindMatchingTypeMethod(klass: any): boolean {
if (typeof klass.findMatchingType === 'function') {
return true;
}
return false;
}

public static serialize(data: any, type: string, format: string): any {
if (data == undefined) {
return data;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Validates if data contains all required attributes from the attributeTypeMap.
*
* @param data - The data object to validate
* @param attributeTypeMap - Array of attribute metadata including required flag
* @returns true if all required attributes are present in data, false otherwise
*/
export function instanceOfType(data: any, attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string, required: boolean}>): boolean {
for (const attribute of attributeTypeMap) {
if (attribute.required) {
// Check both that the property exists AND that it's not undefined.
// This is important because `data[attribute.baseName] === undefined` alone
// would be true for both missing properties and properties explicitly set to undefined,
// while `!(attribute.baseName in data)` distinguishes between these cases.
// For proper OpenAPI validation, required fields must actually be present in the data.
if (!(attribute.baseName in data) || data[attribute.baseName] === undefined) {
Comment thread
ksvirkou-hubspot marked this conversation as resolved.
Outdated
return false;
}
}
}

return true;
}

/**
* Attempts to find a matching type from an array of possible types by validating
* the data against each type's attribute requirements.
*
* @param data - The data object to match
* @param types - Array of possible type constructors
* @returns The name of the matching type, or undefined if no match found
*/
export function findMatchingType(data: any, types: Array<any>): string | undefined {
for (const type of types) {
if (instanceOfType(data, type.getAttributeTypeMap())) {
return type.name;
}
}

return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
{{#tsImports}}
import { {{classname}} } from '{{filename}}{{importFileExtension}}';
{{/tsImports}}
{{#oneOf}}
{{#-first}}
import { findMatchingType } from '../models/TypeMatcher{{importFileExtension}}';
{{/-first}}
{{/oneOf}}
{{^oneOf}}
import { HttpFile } from '../http/http{{importFileExtension}}';
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.

are we sure this is not used in the oneOf case?

{{/oneOf}}

{{#description}}
/**
Expand Down Expand Up @@ -46,13 +53,14 @@ export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
{{/hasDiscriminatorWithNonEmptyMapping}}

{{^isArray}}
static {{#parent}}override {{/parent}}readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
static {{#parent}}override {{/parent}}readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string, required: boolean}> = [
{{#vars}}
{
"name": "{{name}}",
"baseName": "{{baseName}}",
"type": "{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}",
"format": "{{dataFormat}}"
"format": "{{dataFormat}}",
"required": {{required}}
}{{^-last}},
{{/-last}}
{{/vars}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
{{#hasImports}}
import {
{{#imports}}
{{{.}}}{{importFileExtension}},
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.

why is this not needed anymore?

{{/imports}}
} from './';

{{/hasImports}}
/**
* @type {{classname}}
* Type
Expand Down Expand Up @@ -37,4 +29,16 @@ export class {{classname}}Class {

static readonly mapping: {[index: string]: string} | undefined = undefined;
{{/hasDiscriminatorWithNonEmptyMapping}}

private static readonly arrayOfTypes: Array<{{#oneOf}}typeof {{{.}}}{{^-last}} | {{/-last}}{{/oneOf}}> = [{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}];

/**
* Determines which oneOf schema matches the provided data.
*
* @param data - The data object to match against oneOf schemas
* @returns The name of the matching type, or undefined if no unique match is found
*/
public static findMatchingType(data: any): string | undefined {
return findMatchingType(data, this.arrayOfTypes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
openapi: 3.0.0
info:
version: 1.0.0
title: testing oneOf
servers:
- url: http://localhost:3000
paths:
/test:
get:
operationId: testWithoutDiscriminator
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PetResponse'
/test-discriminator:
get:
operationId: testDiscriminator
responses:
200:
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PetDiscriminatorResponse'
components:
schemas:
PetDiscriminatorResponse:
discriminator:
propertyName: petType
mapping:
cat: "#/components/schemas/Cat"
dog: "#/components/schemas/Dog"
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
PetResponse:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
Cat:
type: object
properties:
name:
type: string
petType:
type: string
required:
- name
- petType
Dog:
type: object
properties:
bark:
type: string
petType:
type: string
required:
- bark
- petType

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions samples/client/echo_api/typescript/build/models/Bird.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions samples/client/echo_api/typescript/build/models/Category.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions samples/client/echo_api/typescript/build/models/DataQuery.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 17 additions & 9 deletions samples/client/echo_api/typescript/build/models/DefaultValue.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading