Skip to content

Incorrect C# property mapping with oneOf componet schema & null #6776

@awesomenath

Description

@awesomenath

What are you generating using Kiota, clients or plugins?

API Client/SDK

In what context or format are you using Kiota?

Nuget tool

Client library/SDK language

Csharp

Describe the bug

I'm trying to use Kiota with a request with mapping oneOf that has one component schema and null, but Kiota can't seem to map this to a nullable string which is what I was expecting to resolve to the underlying type.

Endpoint:

    customer-create:
      title: CustomerCreate
      x-stoplight:
        id: t35cm8w8ydouf
      description: Represents a customer entity when creating customers.
      type: object
      additionalProperties: false
      properties:
        id:
          $ref: "#/components/schemas/customer_id"
          readOnly: true
        name:
          oneOf:
            - $ref: "#/components/schemas/name"
            - type: "null"
          description: Full name of this customer. Required when creating transactions where `collection_mode` is `manual` (invoices).
        email:
          $ref: "#/components/schemas/email"
          description: Email address for this customer.
        marketing_consent:
          type: boolean
          default: false
          description: |-
            Whether this customer opted into marketing from you. `false` unless customers check the marketing consent box
            when using Paddle Checkout. Set automatically by Paddle.
          readOnly: true
        custom_data:
          description: Your own structured key-value data.
          oneOf:
            - $ref: "#/components/schemas/custom_data"
            - type: "null"
        locale:
          description: Valid IETF BCP 47 short form locale tag. If omitted, defaults to `en`.
          default: en
          type: string
        import_meta:
          description: Import information for this entity. `null` if this entity is not imported.
          oneOf:
            - $ref: "#/components/schemas/import_meta"
            - type: "null"
          readOnly: true
      required:
        - email
      x-tags:
        - "v1: Entities"

Name Schema:

  name:
      x-stoplight:
        id: 4f2fk6rigfqro
      title: Name
      description: Full name.
      type: string
      maxLength: 1024

Customer Id schema:

customer_id:
      title: Customer ID
      x-stoplight:
        id: 6bbb1nk365bfp
      description: Unique Paddle ID for this customer entity, prefixed with `ctm_`.
      type: string
      examples:
        - ctm_01grnn4zta5a1mf02jjze7y2ys
      pattern: ^ctm_[a-z\d]{26}$

Email schema:

    email:
      x-stoplight:
        id: 7zm82w155qlwi
      title: Email address
      description: Email address for this entity.
      type: string
      format: email
      maxLength: 320
      minLength: 1
      examples:
        - test@paddle.com

Ends up with the following C# code, with some removed lines for brevity:

/// <summary>
/// Represents a customer entity when creating customers.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CustomerCreate : IParsable
{
        /// <summary>Email address for this customer.</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public string? Email { get; set; }
#nullable restore
#else
        public string Email { get; set; }
#endif
        /// <summary>Unique Paddle ID for this customer entity, prefixed with `ctm_`.</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public string? Id { get; private set; }
#nullable restore
#else
        public string Id { get; private set; }
#endif
        /// <summary>Full name of this customer. Required when creating transactions where `collection_mode` is `manual` (invoices).</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name? Name { get; set; }
#nullable restore
#else
        public global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name Name { get; set; }
#endif

        // <summary>
        /// Instantiates a new <see cref="global::Paddle.Client.Models.CustomerCreate"/> and sets the default values.
        /// </summary>
        public CustomerCreate()
        {
            Locale = "en";
        }
        /// <summary>
        /// Creates a new instance of the appropriate class based on discriminator value
        /// </summary>
        /// <returns>A <see cref="global::Paddle.Client.Models.CustomerCreate"/></returns>
        /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
        public static global::Paddle.Client.Models.CustomerCreate CreateFromDiscriminatorValue(IParseNode parseNode)
        {
            _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
            return new global::Paddle.Client.Models.CustomerCreate();
        }
        /// <summary>
        /// The deserialization information for the current model
        /// </summary>
        /// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
        public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
        {
            return new Dictionary<string, Action<IParseNode>>
            {
                { "custom_data", n => { CustomData = n.GetObjectValue<global::Paddle.Client.Models.CustomerCreate.CustomerCreate_custom_data>(global::Paddle.Client.Models.CustomerCreate.CustomerCreate_custom_data.CreateFromDiscriminatorValue); } },
                { "email", n => { Email = n.GetStringValue(); } },
                { "id", n => { Id = n.GetStringValue(); } },
                { "import_meta", n => { ImportMeta = n.GetObjectValue<global::Paddle.Client.Models.CustomerCreate.CustomerCreate_import_meta>(global::Paddle.Client.Models.CustomerCreate.CustomerCreate_import_meta.CreateFromDiscriminatorValue); } },
                { "locale", n => { Locale = n.GetStringValue(); } },
                { "marketing_consent", n => { MarketingConsent = n.GetBoolValue(); } },
                { "name", n => { Name = n.GetObjectValue<global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name>(global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name.CreateFromDiscriminatorValue); } },
            };
        }
        /// <summary>
        /// Serializes information the current object
        /// </summary>
        /// <param name="writer">Serialization writer to use to serialize this model</param>
        public virtual void Serialize(ISerializationWriter writer)
        {
            _ = writer ?? throw new ArgumentNullException(nameof(writer));
            writer.WriteObjectValue<global::Paddle.Client.Models.CustomerCreate.CustomerCreate_custom_data>("custom_data", CustomData);
            writer.WriteStringValue("email", Email);
            writer.WriteStringValue("locale", Locale);
            writer.WriteObjectValue<global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name>("name", Name);
        }

        /// <summary>
        /// Composed type wrapper for classes <see cref="global::Paddle.Client.Models.CustomerCreate_nameMember1"/>, <see cref="global::Paddle.Client.Models.Name"/>
        /// </summary>
        [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
        public partial class CustomerCreate_name : IComposedTypeWrapper, IParsable
        {
            /// <summary>Composed type representation for type <see cref="global::Paddle.Client.Models.CustomerCreate_nameMember1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
            public global::Paddle.Client.Models.CustomerCreate_nameMember1? CustomerCreateNameMember1 { get; set; }
#nullable restore
#else
            public global::Paddle.Client.Models.CustomerCreate_nameMember1 CustomerCreateNameMember1 { get; set; }
#endif
            /// <summary>Composed type representation for type <see cref="global::Paddle.Client.Models.Name"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
            public global::Paddle.Client.Models.Name? Name { get; set; }
#nullable restore
#else
            public global::Paddle.Client.Models.Name Name { get; set; }
#endif
            /// <summary>
            /// Creates a new instance of the appropriate class based on discriminator value
            /// </summary>
            /// <returns>A <see cref="global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name"/></returns>
            /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
            public static global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name CreateFromDiscriminatorValue(IParseNode parseNode)
            {
                _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
                var mappingValue = parseNode.GetChildNode("")?.GetStringValue();
                var result = new global::Paddle.Client.Models.CustomerCreate.CustomerCreate_name();
                if("".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
                {
                    result.CustomerCreateNameMember1 = new global::Paddle.Client.Models.CustomerCreate_nameMember1();
                }
                else if("".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
                {
                    result.Name = new global::Paddle.Client.Models.Name();
                }
                return result;
            }
            /// <summary>
            /// The deserialization information for the current model
            /// </summary>
            /// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
            public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
            {
                if(CustomerCreateNameMember1 != null)
                {
                    return CustomerCreateNameMember1.GetFieldDeserializers();
                }
                else if(Name != null)
                {
                    return Name.GetFieldDeserializers();
                }
                return new Dictionary<string, Action<IParseNode>>();
            }
            /// <summary>
            /// Serializes information the current object
            /// </summary>
            /// <param name="writer">Serialization writer to use to serialize this model</param>
            public virtual void Serialize(ISerializationWriter writer)
            {
                _ = writer ?? throw new ArgumentNullException(nameof(writer));
                if(CustomerCreateNameMember1 != null)
                {
                    writer.WriteObjectValue<global::Paddle.Client.Models.CustomerCreate_nameMember1>(null, CustomerCreateNameMember1);
                }
                else if(Name != null)
                {
                    writer.WriteObjectValue<global::Paddle.Client.Models.Name>(null, Name);
                }
            }
        }
}

Customer_nameMember1 class:

[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
#pragma warning disable CS1591
public partial class Customer_nameMember1 : IParsable
#pragma warning restore CS1591
{
    /// <summary>
    /// Creates a new instance of the appropriate class based on discriminator value
    /// </summary>
    /// <returns>A <see cref="global::Paddle.Client.Models.Customer_nameMember1"/></returns>
    /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
    public static global::Paddle.Client.Models.Customer_nameMember1 CreateFromDiscriminatorValue(IParseNode parseNode)
    {
        _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
        return new global::Paddle.Client.Models.Customer_nameMember1();
    }
    /// <summary>
    /// The deserialization information for the current model
    /// </summary>
    /// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
    public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
    {
        return new Dictionary<string, Action<IParseNode>>
        {
        };
    }
    /// <summary>
    /// Serializes information the current object
    /// </summary>
    /// <param name="writer">Serialization writer to use to serialize this model</param>
    public virtual void Serialize(ISerializationWriter writer)
    {
        _ = writer ?? throw new ArgumentNullException(nameof(writer));
    }
}

Name class:

/// <summary>
/// Full name.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class Name : IParsable
{
    /// <summary>
    /// Creates a new instance of the appropriate class based on discriminator value
    /// </summary>
    /// <returns>A <see cref="global::Paddle.Client.Models.Name"/></returns>
    /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
    public static global::Paddle.Client.Models.Name CreateFromDiscriminatorValue(IParseNode parseNode)
    {
        _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
        return new global::Paddle.Client.Models.Name();
    }
    /// <summary>
    /// The deserialization information for the current model
    /// </summary>
    /// <returns>A IDictionary&lt;string, Action&lt;IParseNode&gt;&gt;</returns>
    public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers()
    {
        return new Dictionary<string, Action<IParseNode>>
        {
        };
    }
    /// <summary>
    /// Serializes information the current object
    /// </summary>
    /// <param name="writer">Serialization writer to use to serialize this model</param>
    public virtual void Serialize(ISerializationWriter writer)
    {
        _ = writer ?? throw new ArgumentNullException(nameof(writer));
    }
}

Email and Id map to nullable strings but Name ends up with these classes that I can't access any values from and can't use correctly.

Expected behavior

I expect to either be able to use the class type with a value that will deserialise / serialise correctly or have it resolve to a nullable string.

How to reproduce

  1. Generate Kiota client e.g. kiota generate -l csharp --namespace-name Paddle.Client --openapi https://github.com/PaddleHQ/paddle-openapi/raw/refs/heads/main/v1/openapi.yaml -o ./Paddle.Client -c PaddleApiClient --additional-data false --exclude-backward-compatible --ll debug
  2. Attempt to use new CustomerCreate type e.g.
var newCustomer = new Paddle.Client.Models.CustomerCreate()
{
    Email = "test@example.com",
    Name = new CustomerCreate.CustomerCreate_name()
    {
        Name = "Test Name"
    }
};

Open API description file

https://github.com/PaddleHQ/paddle-openapi/blob/main/v1/openapi.yaml

Kiota Version

1.28.0+57130b1b1db3bc5c060498682f41e20c8ae089f2

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

No response

Known Workarounds

No response

Configuration

  • OS: Windows 11
  • Architecture: x64

Debug output

Click to expand log
Information: KiotaBuilder Cleaning output directory .\Paddle.Client
Debug: KiotaBuilder kiota version 1.28.0
Debug: KiotaBuilder cache file C:\AppData\Local\Temp\kiota\cache\generation\CD7AA69F99657234A7E1A9689AE0D0318B0FA399E67960409DE8DA459839510E\openapi.yaml is up to date and clearCache is False, using it
Information: KiotaBuilder loaded description from remote source
Debug: KiotaBuilder step 1 - reading the stream - took 00:00:00.0060274
Warning: KiotaBuilder OpenAPI warning: #/ - Multiple servers entries were found in the OpenAPI description. Only the first one will be used. The root URL can be set manually with the request adapter.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation_scenario_config is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation-update is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation-create is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation-run is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema simulation-run-includes is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/ - The schema report is a polymorphic type but does not define a discriminator. This will result in serialization errors.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/email - The format email is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/error/properties/error/properties/documentation_url - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/image_url - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/pagination/properties/next - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/product-create/properties/image_url/oneOf/0 - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/product-update/properties/image_url/oneOf/2 - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/product.created/allOf/1/properties/data/properties/image_url/oneOf/2 - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/product.imported/allOf/1/properties/data/properties/image_url/oneOf/2 - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/product.updated/allOf/1/properties/data/properties/image_url/oneOf/2 - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/subscription_management_urls/properties/update_payment_method - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/subscription_management_urls/properties/cancel - The format uri is not supported by Kiota and the string type will be used.
Warning: KiotaBuilder OpenAPI warning: #/components/schemas/transaction-subscription-product-create/properties/image_url/oneOf/2 - The format uri is not supported by Kiota and the string type will be used.
Debug: KiotaBuilder step 2 - parsing the document - took 00:00:00.5502161
Debug: KiotaBuilder step 3 - updating generation configuration from kiota extension - took 00:00:00.0000617
Debug: KiotaBuilder step 4 - filtering API paths with patterns - took 00:00:00.0029282
Information: KiotaBuilder Client root URL set to https://api.paddle.com
Debug: KiotaBuilder step 5 - checking whether the output should be updated - took 00:00:00.0312706
Debug: KiotaBuilder step 6 - create uri space - took 00:00:00.0020808
Debug: KiotaBuilder InitializeInheritanceIndex 00:00:00.0025049
Warning: KiotaBuilder Discriminator name is not inherited from name.
Warning: KiotaBuilder Discriminator subscription_id is not inherited from subscription_id.
Warning: KiotaBuilder Discriminator business_id is not inherited from business_id.
Warning: KiotaBuilder Discriminator discount_code is not inherited from discount_code.
Warning: KiotaBuilder Discriminator timestamp is not inherited from timestamp.
Warning: KiotaBuilder Discriminator customer_id is not inherited from customer_id.
Warning: KiotaBuilder Discriminator address_id is not inherited from address_id.
Warning: KiotaBuilder Discriminator external_id is not inherited from external_id.
Warning: KiotaBuilder Discriminator image_url is not inherited from image_url.
Warning: KiotaBuilder Discriminator empty_string is not inherited from empty_string.
Warning: KiotaBuilder Discriminator discount_id is not inherited from discount_id.
Warning: KiotaBuilder Discriminator document_number is not inherited from document_number.
Warning: KiotaBuilder Discriminator apikey-description is not inherited from apikeyDescription.
Warning: KiotaBuilder Discriminator payment_method_id is not inherited from payment_method_id.
Warning: KiotaBuilder Discriminator transaction_id is not inherited from transaction_id.
Warning: KiotaBuilder Discriminator price_id is not inherited from price_id.
Warning: KiotaBuilder Discriminator product_id is not inherited from product_id.
Warning: KiotaBuilder Discriminator updated_at is not inherited from updated_at.
Warning: KiotaBuilder Discriminator adjustment-tax-rates-used is not inherited from updated_data_tax_rates_usedMember1.
Warning: KiotaBuilder Discriminator created_at is not inherited from created_at.
Warning: KiotaBuilder Discriminator adjustment-tax-rates-used is not inherited from created_data_tax_rates_usedMember1.
Debug: KiotaBuilder CreateRequestBuilderClass 00:00:00
Debug: KiotaBuilder MapTypeDefinitions 00:00:00.0226759
Debug: KiotaBuilder TrimInheritedModels 00:00:00
Debug: KiotaBuilder CleanUpInternalState 00:00:00
Debug: KiotaBuilder step 7 - create source model - took 00:00:00.3758099
Debug: KiotaBuilder 244ms: Language refinement applied
Debug: KiotaBuilder step 8 - refine by language - took 00:00:00.2453384
Debug: KiotaBuilder step 9 - writing files - took 00:00:00.6515641
Debug: KiotaBuilder cache file C:\AppData\Local\Temp\kiota\cache\generation\CD7AA69F99657234A7E1A9689AE0D0318B0FA399E67960409DE8DA459839510E\openapi.yaml is up to date and clearCache is False, using it
Information: KiotaBuilder loaded description from remote source
Debug: KiotaBuilder step 10 - writing lock file - took 00:00:00.0333733
Debug: KiotaBuilder Api manifest path: apimanifest.json

Other information

It correctly resolves to a string however when not using the oneOf scenario. The Open API file has many usages of this scenario and it seems that most if not all them with oneOf usages result in the same non-functional mapping code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    CsharpPull requests that update .net codetype:bugA broken experiencetype:investigationInvestigation work, output should be a document detailing findings or a prototype

    Type

    No type

    Projects

    Status

    Needs Triage 🔍

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions