Repository: microsoft/kiota (csharp)

Issue:
Incorrect deserialization behavior from generated TS Client with `oneOf`.
### 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.
> - OpenAPI Spec `OneOf`: https://github.com/Software-Hardware-Integration-Lab/OpenAPI/blob/8e8fabe8651c268258946ca1bc4c4788d2019880/specs/SHIELD.json#L1022
> 
> 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](https://github.com/microsoft/kiota/issues/2720#issuecomment-3249655680)_

# 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

<details><summary>Click to expand log</summary>
```


<log output here>

```
</details>


### Other information

_No response_

Pull request:
## 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`):

```typescript
// 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:

```csharp
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:

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

## Result

```typescript
// 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

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



<details>

<summary>Original prompt</summary>


----

*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.
> - OpenAPI Spec `OneOf`: https://github.com/Software-Hardware-Integration-Lab/OpenAPI/blob/8e8fabe8651c268258946ca1bc4c4788d2019880/specs/SHIELD.json#L1022
> 
> 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](https://github.com/microsoft/kiota/issues/2720#issuecomment-3249655680)_

# 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

<details><summary>Click to expand log</summary>
```


<log output here>

```
</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).

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1253fb18de..32d91e1da4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Changed
 
+- Fixed a bug where TypeScript deserialization of oneOf types with inheritance would incorrectly place derived type properties in additionalProperties. [#6896](https://github.com/microsoft/kiota/issues/6896)
+
 ## [1.29.0] - 2025-10-23
 
 ### Added
@@ -1690,4 +1692,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Initial GitHub release
 
-
+
diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
index d4b1a9b6af..72aa88dfcf 100644
--- a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
+++ b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs
@@ -10,6 +10,79 @@ namespace Kiota.Builder.Writers.TypeScript;
 
 public class CodeFunctionWriter(TypeScriptConventionService conventionService) : BaseElementWriter<CodeFunction, TypeScriptConventionService>(conventionService)
 {
+    /// <summary>
+    /// Sorts CodeType objects by inheritance hierarchy.
+    /// Derived classes come before their base classes to ensure proper deserialization.
+    /// </summary>
+    private static List<CodeType> SortTypesByInheritance(IEnumerable<CodeType> types)
+    {
+        ArgumentNullException.ThrowIfNull(types);
+
+        var typesList = types.ToList();
+        if (typesList.Count <= 1)
+            return typesList;
+
+        // Helper function to check if type1 derives from type2
+        bool DerivesFrom(CodeType type1, CodeType type2)
+        {
+            // Handle both CodeClass and CodeInterface (TypeScript uses interfaces)
+            if (type1.TypeDefinition is CodeClass class1 && type2.TypeDefinition is CodeClass class2)
+            {
+                return class1.DerivesFrom(class2);
+            }
+            else if (type1.TypeDefinition is CodeInterface interface1 && type2.TypeDefinition is CodeInterface interface2)
+            {
+                // For interfaces, check if interface1's OriginalClass derives from interface2's OriginalClass
+                if (interface1.OriginalClass != null && interface2.OriginalClass != null)
+                {
+                    return interface1.OriginalClass.DerivesFrom(interface2.OriginalClass);
+                }
+            }
+            return false;
+        }
+
+        // Sort by inheritance: derived classes before base classes
+        // Use a topological sort where we prioritize "leaf" classes (most derived)
+        var sorted = new List<CodeType>();
+        var processed = new HashSet<CodeType>();
+
+        // Keep processing until all types are sorted
+        while (processed.Count < typesList.Count)
+        {
+            var addedInThisIteration = false;
+
+            foreach (var type in typesList)
+            {
+                if (processed.Contains(type))
+                    continue;
+
+                // Check if there are any unprocessed types that derive from this type
+                var hasUnprocessedDerivedTypes = typesList.Any(t =>
+                    !processed.Contains(t) && t != type && DerivesFrom(t, type));
+
+                // Add this type if no unprocessed derived types exist
+                // This ensures derived classes are added before their base classes
+                if (!hasUnprocessedDerivedTypes)
+                {
+                    sorted.Add(type);
+                    processed.Add(type);
+                    addedInThisIteration = true;
+                }
+            }
+
+            // If we didn't add anything in this iteration, break to avoid infinite loop
+            // This handles cases where there are circular dependencies or no inheritance relationships
+            if (!addedInThisIteration)
+            {
+                // Add remaining types in their original order
+                sorted.AddRange(typesList.Where(t => !processed.Contains(t)));
+                break;
+            }
+        }
+
+        return sorted;
+    }
+
     private static readonly HashSet<string> customSerializationWriters = new(StringComparer.OrdinalIgnoreCase) { "writeObjectValue", "writeCollectionOfObjectValues" };
     private const string FactoryMethodReturnType = "((instance?: Parsable) => Record<string, (node: ParseNode) => void>)";
 
@@ -651,7 +724,7 @@ private void WritePropertyDeserializationBlock(CodeProperty otherProp, CodeParam
         }
         else if (GetOriginalComposedType(otherProp.Type) is { } composedType)
         {
-            var expression = string.Join(" ?? ", composedType.Types.Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFile, composedType.IsCollection)}"));
+            var expression = string.Join(" ?? ", SortTypesByInheritance(composedType.Types).Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFile, composedType.IsCollection)}"));
             writer.WriteLine($"\"{otherProp.WireName}\": n => {{ {paramName}.{propName} = {expression};{suffix} }},");
         }
         else


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs
index ef59b56a42..02e13813af 100644
--- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs
+++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs
@@ -1717,5 +1717,69 @@ public void WritesByteArrayPropertyDeserialization()
         var result = tw.ToString();
         Assert.Contains("\"property\": n => { model.property = n.getByteArrayValue(); }", result, StringComparison.Ordinal);
     }
+
+    [Fact]
+    public async Task WritesOneOfWithInheritanceDeserializationAsync()
+    {
+        // Create base class "Device"
+        var baseClass = TestHelper.CreateModelClass(root, "Device");
+        baseClass.AddProperty(new CodeProperty
+        {
+            Name = "deviceId",
+            Type = new CodeType { Name = "string" },
+            Kind = CodePropertyKind.Custom,
+        });
+
+        // Create derived class "ManagedPrivilegedDevice" that inherits from "Device"
+        var derivedClass = TestHelper.CreateModelClass(root, "ManagedPrivilegedDevice");
+        derivedClass.StartBlock.Inherits = new CodeType
+        {
+            Name = "Device",
+            TypeDefinition = baseClass,
+        };
+        derivedClass.AddProperty(new CodeProperty
+        {
+            Name = "privilegeLevel",
+            Type = new CodeType { Name = "string" },
+            Kind = CodePropertyKind.Custom,
+        });
+
+        // Create a model with a oneOf property containing both types
+        var parentClass = TestHelper.CreateModelClass(root, "Container");
+        var composedType = new CodeUnionType { Name = "DeviceUnion" };
+
+        // Add types in alphabetical order: Device comes before ManagedPrivilegedDevice
+        // But ManagedPrivilegedDevice should be checked first because it's derived from Device
+        composedType.AddType(
+            new CodeType { Name = "Device", TypeDefinition = baseClass },
+            new CodeType { Name = "ManagedPrivilegedDevice", TypeDefinition = derivedClass }
+        );
+
+        parentClass.AddProperty(new CodeProperty
+        {
+            Name = "deviceProperty",
+            Type = composedType,
+            Kind = CodePropertyKind.Custom,
+        });
+
+        await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root);
+        var deserializerFunction = root.FindChildByName<CodeFunction>($"deserializeIntoContainer");
+        Assert.NotNull(deserializerFunction);
+        var parentNS = deserializerFunction.GetImmediateParentOfType<CodeNamespace>();
+        Assert.NotNull(parentNS);
+        parentNS.TryAddCodeFile("foo", deserializerFunction);
+        writer.Write(deserializerFunction);
+        var result = tw.ToString();
+
+        // The generated code should check ManagedPrivilegedDevice first (derived class)
+        // before checking Device (base class), regardless of alphabetical order
+        // This ensures proper deserialization when the derived type is received
+        var managedDeviceIndex = result.IndexOf("createManagedPrivilegedDeviceFromDiscriminatorValue", StringComparison.Ordinal);
+        var deviceIndex = result.IndexOf("createDeviceFromDiscriminatorValue", StringComparison.Ordinal);
+
+        Assert.True(managedDeviceIndex > 0, "Should contain ManagedPrivilegedDevice deserialization");
+        Assert.True(deviceIndex > 0, "Should contain Device deserialization");
+        Assert.True(managedDeviceIndex < deviceIndex, "ManagedPrivilegedDevice should appear before Device in the deserialization chain");
+    }
 }