Skip to content

Fix oneOf deserialization ordering for TypeScript with inheritance hierarchy#7033

Merged
gavinbarron merged 8 commits intomainfrom
copilot/fix-ts-client-deserialization
Oct 30, 2025
Merged

Fix oneOf deserialization ordering for TypeScript with inheritance hierarchy#7033
gavinbarron merged 8 commits intomainfrom
copilot/fix-ts-client-deserialization

Conversation

Copy link
Contributor

Copilot AI commented Oct 23, 2025

Problem

When generating TypeScript clients from OpenAPI specs with oneOf containing types with inheritance relationships, the deserialization logic only worked correctly for the first type in an alphabetically sorted list. This caused properties of derived types to be incorrectly placed in additionalProperties instead of being properly deserialized into the root object.

For example, given a oneOf containing Device (base class) and ManagedPrivilegedDevice (derived from Device):

// Before fix - Device checked first (alphabetically)
n.getObjectValue<Device>(createDeviceFromDiscriminatorValue) ?? 
n.getObjectValue<ManagedPrivilegedDevice>(createManagedPrivilegedDeviceFromDiscriminatorValue)

When receiving a ManagedPrivilegedDevice, the deserialization would match the base Device type first, causing ManagedPrivilegedDevice-specific properties like privilegeLevel to be incorrectly placed in additionalProperties.

Root Cause

The CodeComposedTypeBase.Types property returns types ordered alphabetically by name. The deserialization code generation in CodeFunctionWriter.cs (line 654) used this alphabetically sorted list directly, ignoring inheritance relationships. This meant base classes could be checked before their derived classes, causing incorrect deserialization behavior.

Solution

Added a SortTypesByInheritance helper method that uses topological sorting to order types by their inheritance hierarchy, ensuring derived classes are always checked before their base classes during deserialization:

private static List<CodeType> SortTypesByInheritance(IEnumerable<CodeType> types)
{
    // Uses topological sort to ensure derived classes come before base classes
    // Supports both CodeClass and CodeInterface (TypeScript uses interfaces after refinement)
    // Includes cycle detection and graceful handling of types with no inheritance
}

Updated the deserialization code generation to use this sorted list:

var expression = string.Join(" ?? ", SortTypesByInheritance(composedType.Types)
    .Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFile, composedType.IsCollection)}"));

Result

// After fix - ManagedPrivilegedDevice checked first (inheritance-aware)
n.getObjectValue<ManagedPrivilegedDevice>(createManagedPrivilegedDeviceFromDiscriminatorValue) ?? 
n.getObjectValue<Device>(createDeviceFromDiscriminatorValue)

Now when a ManagedPrivilegedDevice is received, it correctly matches the more specific type first, and all properties are properly deserialized to the root object.

Testing

  • ✅ Added comprehensive test WritesOneOfWithInheritanceDeserializationAsync to verify inheritance-based ordering
  • ✅ All 152 TypeScript tests pass
  • ✅ All 1,821 Kiota.Builder tests pass (5 unrelated GitHub API rate limit failures)
  • ✅ No security vulnerabilities introduced

Credit

Thanks to @gavinbarron for identifying the root cause and suggesting the sorting approach in the issue comments.

Fixes #6896

Original prompt

This section details on the original issue you should resolve

<issue_title>Incorrect deserialization behavior from generated TS Client with oneOf.</issue_title>
<issue_description>### What are you generating using Kiota, clients or plugins?

API Client/SDK

In what context or format are you using Kiota?

Windows executable

Client library/SDK language

TypeScript

Describe the bug

This is to follow up the comment below:

Happening in typescript.

Behavior:
Types are generated correctly; execution time deserialization puts the Managed Privilege Device properties into additional properties instead of using the privileged device as the resultant type and putting them on the root object.
(The second item/type in the OneOf list is never deserializes, only the first time. If an object of the second type is received, its properties are put into additional properties instead of the root object as expected.)

Originally posted by @elliot-huffman in #2720

Issue Summary

Problem Area: Microsoft Kiota OpenAPI client code generator
Component: Deserialization logic for schemas using oneOf
Spec Reference: SHIELD.json#L1022 (https://github.com/Software-Hardware-Integration-Lab/OpenAPI/blob/8e8fabe8651c268258946ca1bc4c4788d2019880/specs/SHIELD.json#L1022)

Description

When generating TypeScript clients using Kiota from an OpenAPI schema that includes both direct properties and a oneOf construct, the deserialization behavior is incorrect.

The generated types appear correct.

However, at runtime, deserialization only works for the first type listed in the oneOf array.

If an object matching the second type is received, its properties are incorrectly placed into the additionalProperties map rather than being deserialized into the expected root object.

As a result, the schema-level properties are not present in the deserialized object when the second oneOf type is used.

We had opened a Microsoft Support ticket about this.
The support team routed us to the issue board here.
Support Service request number for your reference: 2509050040003673.

Expected behavior

The deserializer should correctly identify and instantiate any of the types listed in the oneOf array.
Schema-level properties should be preserved and placed on the root object regardless of which oneOf type is matched.

Actual Behavior
Only the first oneOf type is deserialized correctly.
Other types are ignored, and their properties are treated as additionalProperties.

How to reproduce

  1. Use Kiota to generate a TypeScript client from the provided OpenAPI spec.
  2. Send or receive an object matching the second type in the oneOf list.
  3. Observe that the deserialized object does not match the expected structure.

Open API description file

https://github.com/Software-Hardware-Integration-Lab/OpenAPI/blob/8e8fabe8651c268258946ca1bc4c4788d2019880/specs/SHIELD.json#L1022

Kiota Version

1.0.0-preview.97

Latest Kiota version known to work for scenario above?(Not required)

No response

Known Workarounds

No response

Configuration

No response

Debug output

Click to expand log ```
</details>


### Other information

_No response_</issue_description>

## Comments on the Issue (you are @copilot in this section)

<comments>
<comment_new><author>@baywet</author><body>
I have transitioned to a new team and won't be replying to this issue/pull request moving forward.
Thank you for your understanding.

(Andrew has left the company around February 2025)</body></comment_new>
<comment_new><author>@gavinbarron</author><body>
Thanks for the info @ferrywlto this ordering is specicfically an issue caused by the .Select used to generate those lines of code here https://github.com/microsoft/kiota/blob/883fbe7317c7cbed7c473555c889dd7069f9aa9b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs#L654

Based on a super primative test the elements in the composedType list are ordered alphabetically. I renamed PrivilegedDevice to APrivilegedDevice and found that the ordering in deserialization would now work in this case.</body></comment_new>
<comment_new><author>@gavinbarron</author><body>
Take this with a grain of salt because I'm not an OpenAPI expert by any stretch, but that `oneOf` usage looks correct and appropriate to me.

The workaround is to rename your types such that the lowest type in the inheritance chain is the first alphabetically and the top class is last. Which I acknowledge is ugly as all get out, but it will work given the current state of the builder logic.

The code change to Kiota that I would propose is to add a helper method that sorts based on the inheritance trees. that would be used at t...

</details>

Fixes microsoft/kiota#6896

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ).

- Add SortTypesByInheritance helper method to CodeFunctionWriter
- Sort types by inheritance hierarchy (derived classes before base classes)
- Support both CodeClass and CodeInterface (TypeScript uses interfaces after refinement)
- Add test to verify inheritance-based ordering
- All 152 TypeScript tests pass

Co-authored-by: gavinbarron <7122716+gavinbarron@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix incorrect deserialization behavior in TypeScript client Fix oneOf deserialization ordering for TypeScript with inheritance hierarchy Oct 23, 2025
Copilot AI requested a review from gavinbarron October 23, 2025 19:23
@gavinbarron
Copy link
Contributor

@copilot please add that this closes #6896 and add a changelog entry

- Add changelog entry for oneOf deserialization fix
- Reference both #2720 and #6896 in changelog

Co-authored-by: gavinbarron <7122716+gavinbarron@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 23, 2025

@copilot please add that this closes #6896 and add a changelog entry

Done! Added changelog entry and "Fixes #6896" to the PR description in commit c59d782.

@gavinbarron
Copy link
Contributor

@copilot this is not a fix for #2720, please remove this issue reference

@gavinbarron gavinbarron marked this pull request as ready for review October 23, 2025 20:16
@gavinbarron gavinbarron requested a review from a team as a code owner October 23, 2025 20:16
- Remove #2720 from changelog entry (only keeping #6896)
- Remove "Fixes #2720" from PR description

Co-authored-by: gavinbarron <7122716+gavinbarron@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 23, 2025

@copilot this is not a fix for #2720, please remove this issue reference

Removed reference to #2720 from both the changelog and PR description in commit 28a8f86. Now only references #6896.

@gavinbarron
Copy link
Contributor

@copilot please run dotnet format to correct formatting issues and commit the result

- Remove trailing whitespace in CodeFunctionWriter.cs
- Remove trailing whitespace in CodeFunctionWriterTests.cs

Co-authored-by: gavinbarron <7122716+gavinbarron@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Oct 30, 2025

@copilot please run dotnet format to correct formatting issues and commit the result

Ran dotnet format and committed the formatting fixes in commit 466a36b. The changes remove trailing whitespace from the modified files.

@gavinbarron gavinbarron disabled auto-merge October 30, 2025 21:45
@gavinbarron gavinbarron enabled auto-merge (squash) October 30, 2025 21:46
@gavinbarron gavinbarron merged commit ef26ded into main Oct 30, 2025
290 checks passed
@gavinbarron gavinbarron deleted the copilot/fix-ts-client-deserialization branch October 30, 2025 22:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect deserialization behavior from generated TS Client with oneOf.

3 participants