Skip to content

Typescript client generation doesn't handle allOf correctly when serializing #6511

Open
@maya-auror

Description

@maya-auror

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

I am trying to generate a typescript client for an API that receives polymorphic input described via the allOf property in the Open API document.

For the attached Open API document, Kiota generates the following RequestsMetadata

export const ReproRequestBuilderRequestsMetadata: RequestsMetadata = {
    post: {
        uriTemplate: ReproRequestBuilderUriTemplate,
        responseBodyContentType: "application/json",
        adapterMethodName: "send",
        responseBodyFactory:  createBaseTypeFromDiscriminatorValue,
        requestBodyContentType: "application/json",
        requestBodySerializer: serializeBaseType,
        requestInformationContentSetMethod: "setContentFromParsable",
    },
};

We can see that the responseBodyFactory is set to createBaseTypeFromDiscriminatorValue which correctly uses the discriminator value

export function createBaseTypeFromDiscriminatorValue(parseNode: ParseNode | undefined) : ((instance?: Parsable) => Record<string, (node: ParseNode) => void>) {
    if(!parseNode) throw new Error("parseNode cannot be undefined");
    const mappingValueNode = parseNode?.getChildNode("derivedType");
    if (mappingValueNode) {
        const mappingValue = mappingValueNode.getStringValue();
        if (mappingValue) {
            switch (mappingValue) {
                case "A":
                    return deserializeIntoDerivedTypeA;
                case "B":
                    return deserializeIntoDerivedTypeB;
            }
        }
    }
    return deserializeIntoBaseType;
}

However the requestBodySerializer is set to serializeBaseType which only serializes the properties defined in BaseType

export function serializeBaseType(writer: SerializationWriter, baseType: Partial<BaseType> | undefined | null = {}) : void {
    if (baseType) {
        writer.writeStringValue("derivedType", baseType.derivedType);
    }
}

// The following methods are generated but not used anywhere in the generated code
export function serializeDerivedTypeA(writer: SerializationWriter, derivedTypeA: Partial<DerivedTypeA> | undefined | null = {}) : void {
    if (derivedTypeA) {
        serializeBaseType(writer, derivedTypeA)
        writer.writeStringValue("typeAProperty", derivedTypeA.typeAProperty);
        writer.writeAdditionalData(derivedTypeA.additionalData);
    }
}

export function serializeDerivedTypeB(writer: SerializationWriter, derivedTypeB: Partial<DerivedTypeB> | undefined | null = {}) : void {
    if (derivedTypeB) {
        serializeBaseType(writer, derivedTypeB)
        writer.writeStringValue("typeBProperty", derivedTypeB.typeBProperty);
        writer.writeAdditionalData(derivedTypeB.additionalData);
    }
}

This means if we try to send one of the derived types to the post endpoint, it will never serialize the additional properties but only the properties on the base type

const request: DerivedTypeA = {
    derivedType: 'A',
    typeAProperty: 'valueA',
};

const response = await apiClient.repro.post(request);
// Sends request body containing the following, typeAProperty is ignored
// {
//   "derivedType": "A"
// }

Expected behavior

Serialization and deserialization should both support the use of the discriminator property as defined in the swagger doc. This could look similar to the way that oneOf is currently implemented with Kiota code generation?

export function serializeBaseType(writer: SerializationWriter, baseType: Partial<DerivedTypeA | DerivedTypeB> | undefined | null = {}) : void {
    if (baseType === undefined || baseType === null) return;
    switch (baseType.derivedType) {
        case "A":
            serializeDerivedTypeA(writer, baseType as DerivedTypeA);
            break;
        case "B":
            serializeDerivedTypeB(writer, baseType as DerivedTypeB);
            break;
    }
}

How to reproduce

  1. Unzip the attached swagger.yml file to a folder
  2. In that folder, run
    kiota generate --openapi swagger.yml --language typescript --output ./typescript
    
  3. Open ./typescript/models/index.ts and observe the serializeBaseType method is generated as described

Open API description file

swagger.zip

Kiota Version

1.25.1+1d771798165084d73f745111fd99df79bbea913c

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

No response

Known Workarounds

We can modify the generated code after the fact to replace the serializeBaseType method with the suggested code. However this requires us to manually modify the files after every generation.

Configuration

  • OS: Windows 11
  • Architecture: x64

Debug output

Click to expand log ```

Warning: the TypeScript language is in preview (Preview) some features are not fully supported and source breaking changes will happen with future updates.
dbug: Kiota.Builder.KiotaBuilder[0]
kiota version 1.25.1
info: Kiota.Builder.KiotaBuilder[0]
loaded description from local source
dbug: Kiota.Builder.KiotaBuilder[0]
step 1 - reading the stream - took 00:00:00.0069358
warn: Kiota.Builder.KiotaBuilder[0]
OpenAPI warning: #/ - A servers entry (v3) or host + basePath + schemes properties (v2) was not present in the OpenAPI description. The root URL will need to be set manually with the request adapter.
dbug: Kiota.Builder.KiotaBuilder[0]
step 2 - parsing the document - took 00:00:00.0889994
dbug: Kiota.Builder.KiotaBuilder[0]
step 3 - updating generation configuration from kiota extension - took 00:00:00.0000761
dbug: Kiota.Builder.KiotaBuilder[0]
step 4 - filtering API paths with patterns - took 00:00:00.0044550
warn: Kiota.Builder.KiotaBuilder[0]
No server url found in the OpenAPI document. The base url will need to be set when using the client.
dbug: Kiota.Builder.KiotaBuilder[0]
step 5 - checking whether the output should be updated - took 00:00:00.0178133
dbug: Kiota.Builder.KiotaBuilder[0]
step 6 - create uri space - took 00:00:00.0030187
dbug: Kiota.Builder.KiotaBuilder[0]
InitializeInheritanceIndex 00:00:00.0027230
dbug: Kiota.Builder.KiotaBuilder[0]
CreateRequestBuilderClass 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
MapTypeDefinitions 00:00:00.0036891
dbug: Kiota.Builder.KiotaBuilder[0]
TrimInheritedModels 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
CleanUpInternalState 00:00:00
dbug: Kiota.Builder.KiotaBuilder[0]
step 7 - create source model - took 00:00:00.0572555
dbug: Kiota.Builder.KiotaBuilder[0]
37ms: Language refinement applied
dbug: Kiota.Builder.KiotaBuilder[0]
step 8 - refine by language - took 00:00:00.0375099
dbug: Kiota.Builder.KiotaBuilder[0]
step 9 - writing files - took 00:00:00.0320194
info: Kiota.Builder.KiotaBuilder[0]
loaded description from local source
dbug: Kiota.Builder.KiotaBuilder[0]
step 10 - writing lock file - took 00:00:00.0138642
Generation completed successfully
dbug: Kiota.Builder.KiotaBuilder[0]
Api manifest path: C:\Repos\KiotaBugRepro\apimanifest.json

Hint: use the info command to get the list of dependencies you need to add to your project.
Example: kiota info -d "C:\Repos\KiotaBugRepro\swagger.yml" -l TypeScript

Hint: use the --include-path and --exclude-path options with glob patterns to filter the paths generated.
Example: kiota generate --include-path "**/foo" -d "C:\Repos\KiotaBugRepro\swagger.yml"

</details>


### Other information

_No response_

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Attention 👋TypeScriptPull requests that update Javascript codehelp wantedIssue caused by core project dependency modules or librarytype:bugA broken experience

    Type

    Projects

    Status

    New📃

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions