kiota icon indicating copy to clipboard operation
kiota copied to clipboard

[C#] Bug: OneOf used inside other OneOf causes duplicated fields in composed type

Open fzzle opened this issue 1 year ago • 7 comments

When I generate a client with the following schemas (a OneOf within a OneOf) it duplicates the properties for inner OneOf model.

This is an example of the input I give to Kiota:

{
  "openapi": "3.0.1",
  "info": {
    "title": "Umbraco Delivery API",
    "description": "",
    "version": "Latest"
  },
  "paths": {
    "/umbraco/delivery/api/v1/content/item/{path}": {
      "get": {
        "tags": ["Content"],
        "operationId": "GetContentItemByPath",
        "parameters": [
          {
            "name": "path",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "default": ""
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResponseModelBase"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "ElementModel1": {
        "type": "object",
        "properties": {
          "contentType": {
            "type": "string",
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "ElementModel2": {
        "type": "object",
        "properties": {
          "contentType": {
            "type": "string",
            "readOnly": true
          }
        },
        "additionalProperties": false
      },
      "ResponseModel2": {
        "type": "object",
        "properties": {
          "contentType": {
            "type": "string",
            "readOnly": true
          },
          "first": {
            "$ref": "#/components/schemas/ElementModelBase"
          },
          "second": {
            "$ref": "#/components/schemas/ElementModelBase"
          },
          "third": {
            "$ref": "#/components/schemas/ElementModelBase"
          }
        },
        "additionalProperties": false
      },
      "ResponseModel1": {
        "type": "object",
        "properties": {
          "contentType": {
            "type": "string",
            "readOnly": true
          },
          "first": {
            "$ref": "#/components/schemas/ElementModelBase"
          },
          "second": {
            "$ref": "#/components/schemas/ElementModelBase"
          },
          "third": {
            "$ref": "#/components/schemas/ElementModelBase"
          }
        },
        "additionalProperties": false
      },
      "ResponseModelBase": {
        "type": "object",
        "oneOf": [
          {
            "$ref": "#/components/schemas/ResponseModel1"
          },
          {
            "$ref": "#/components/schemas/ResponseModel2"
          }
        ],
        "additionalProperties": false,
        "discriminator": {
          "propertyName": "contentType",
          "mapping": {
            "a": "#/components/schemas/ResponseModel1",
            "b": "#/components/schemas/ResponseModel2"
          }
        }
      },
      "ElementModelBase": {
        "type": "object",
        "oneOf": [
          {
            "$ref": "#/components/schemas/ElementModel1"
          },
          {
            "$ref": "#/components/schemas/ElementModel2"
          }
        ],
        "additionalProperties": false,
        "discriminator": {
          "propertyName": "contentType",
          "mapping": {
            "c": "#/components/schemas/ElementModel1",
            "d": "#/components/schemas/ElementModel2"
          }
        }
      }
    }
  }
}

And this is the generated ElementModelBase:

// <auto-generated/>
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
namespace Namespace.Models {
    /// <summary>
    /// Composed type wrapper for classes <see cref="ElementModel1"/>, <see cref="ElementModel2"/>
    /// </summary>
    public class ElementModelBase : IComposedTypeWrapper, IParsable 
    {
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModel1 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModel1 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModel2 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModel2 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModelBaseElementModel1 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModelBaseElementModel1 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModelBaseElementModel10 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModelBaseElementModel10 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModelBaseElementModel11 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModelBaseElementModel11 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModelBaseElementModel12 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModelBaseElementModel12 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModelBaseElementModel13 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModelBaseElementModel13 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModelBaseElementModel2 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModelBaseElementModel2 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModelBaseElementModel20 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModelBaseElementModel20 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModelBaseElementModel21 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModelBaseElementModel21 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModelBaseElementModel22 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModelBaseElementModel22 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModelBaseElementModel23 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModelBaseElementModel23 { get; set; }
#endif
        /// <summary>
        /// Creates a new instance of the appropriate class based on discriminator value
        /// </summary>
        /// <returns>A <see cref="ElementModelBase"/></returns>
        /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
        public static ElementModelBase CreateFromDiscriminatorValue(IParseNode parseNode)
        {
            _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
            var mappingValue = parseNode.GetChildNode("contentType")?.GetStringValue();
            var result = new ElementModelBase();
            if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModel1 = new Namespace.Models.ElementModel1();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModel2 = new Namespace.Models.ElementModel2();
            }
            else if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel1 = new Namespace.Models.ElementModel1();
            }
            else if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel10 = new Namespace.Models.ElementModel1();
            }
            else if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel11 = new Namespace.Models.ElementModel1();
            }
            else if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel12 = new Namespace.Models.ElementModel1();
            }
            else if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel13 = new Namespace.Models.ElementModel1();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel2 = new Namespace.Models.ElementModel2();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel20 = new Namespace.Models.ElementModel2();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel21 = new Namespace.Models.ElementModel2();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel22 = new Namespace.Models.ElementModel2();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModelBaseElementModel23 = new Namespace.Models.ElementModel2();
            }
            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(ElementModel1 != null)
            {
                return ElementModel1.GetFieldDeserializers();
            }
            else if(ElementModel2 != null)
            {
                return ElementModel2.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel1 != null)
            {
                return ElementModelBaseElementModel1.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel10 != null)
            {
                return ElementModelBaseElementModel10.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel11 != null)
            {
                return ElementModelBaseElementModel11.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel12 != null)
            {
                return ElementModelBaseElementModel12.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel13 != null)
            {
                return ElementModelBaseElementModel13.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel2 != null)
            {
                return ElementModelBaseElementModel2.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel20 != null)
            {
                return ElementModelBaseElementModel20.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel21 != null)
            {
                return ElementModelBaseElementModel21.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel22 != null)
            {
                return ElementModelBaseElementModel22.GetFieldDeserializers();
            }
            else if(ElementModelBaseElementModel23 != null)
            {
                return ElementModelBaseElementModel23.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(ElementModel1 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModel1);
            }
            else if(ElementModel2 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModel2);
            }
            else if(ElementModelBaseElementModel1 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModelBaseElementModel1);
            }
            else if(ElementModelBaseElementModel10 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModelBaseElementModel10);
            }
            else if(ElementModelBaseElementModel11 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModelBaseElementModel11);
            }
            else if(ElementModelBaseElementModel12 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModelBaseElementModel12);
            }
            else if(ElementModelBaseElementModel13 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModelBaseElementModel13);
            }
            else if(ElementModelBaseElementModel2 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModelBaseElementModel2);
            }
            else if(ElementModelBaseElementModel20 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModelBaseElementModel20);
            }
            else if(ElementModelBaseElementModel21 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModelBaseElementModel21);
            }
            else if(ElementModelBaseElementModel22 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModelBaseElementModel22);
            }
            else if(ElementModelBaseElementModel23 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModelBaseElementModel23);
            }
        }
    }
}

Generated using:

kiota generate --openapi swagger_problem.json -l csharp -n Namespace -o DeliveryApiClientProblem -c DeliveryApiClient -i "**/{path}" --ebc --ll Trace

fzzle avatar Apr 10 '24 18:04 fzzle

Thanks for raising this @fzzle

This looks to be a duplicate of https://github.com/microsoft/kiota/issues/4178. @andreaTP Any chance you can confirm?

andrueastman avatar Apr 11 '24 08:04 andrueastman

Looks very similar, @fzzle any chance you can test using Kiota from my branch ( https://github.com/andreaTP/kiota/tree/initial-fixes-simple-oneOf-py ) with the fixes or from this (non-official)"pre-release" which includes the fixes: https://github.com/andreaTP/kiota-prerelease/releases/tag/v0.0.0-pre%2BandreaTP.initial-fixes-simple-oneOf-py.b46369a

andreaTP avatar Apr 11 '24 09:04 andreaTP

Hi @andrueastman and @andreaTP,

Thanks for the swift reply.

I've tested your branch and I can see that it solves the problem.

The model now looks like this.

// <auto-generated/>
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
namespace Namespace.Models {
    /// <summary>
    /// Composed type wrapper for classes <see cref="ElementModel1"/>, <see cref="ElementModel2"/>
    /// </summary>
    public class ElementModelBase : IComposedTypeWrapper, IParsable 
    {
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel1"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel1? ElementModel1 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel1 ElementModel1 { get; set; }
#endif
        /// <summary>Composed type representation for type <see cref="Namespace.Models.ElementModel2"/></summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
        public Namespace.Models.ElementModel2? ElementModel2 { get; set; }
#nullable restore
#else
        public Namespace.Models.ElementModel2 ElementModel2 { get; set; }
#endif
        /// <summary>
        /// Creates a new instance of the appropriate class based on discriminator value
        /// </summary>
        /// <returns>A <see cref="ElementModelBase"/></returns>
        /// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
        public static ElementModelBase CreateFromDiscriminatorValue(IParseNode parseNode)
        {
            _ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
            var mappingValue = parseNode.GetChildNode("contentType")?.GetStringValue();
            var result = new ElementModelBase();
            if("c".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModel1 = new Namespace.Models.ElementModel1();
            }
            else if("d".Equals(mappingValue, StringComparison.OrdinalIgnoreCase))
            {
                result.ElementModel2 = new Namespace.Models.ElementModel2();
            }
            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(ElementModel1 != null)
            {
                return ElementModel1.GetFieldDeserializers();
            }
            else if(ElementModel2 != null)
            {
                return ElementModel2.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(ElementModel1 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel1>(null, ElementModel1);
            }
            else if(ElementModel2 != null)
            {
                writer.WriteObjectValue<Namespace.Models.ElementModel2>(null, ElementModel2);
            }
        }
    }
}

fzzle avatar Apr 11 '24 16:04 fzzle

@fzzle @andreaTP can you try with the latest preview from yesterday and confirm whether you observe the problem? We've made significant improvements to the handling of allof edge scenarios with #4668 and #4381

baywet avatar May 17 '24 14:05 baywet

Checked and the problem is still present.

andreaTP avatar May 20 '24 10:05 andreaTP

I'm looking forward to this being fixed. We're still using the non-official version from @andreaTP

fzzle avatar Jun 20 '24 08:06 fzzle

cc. @baywet just to be sure that this is under your radar

andreaTP avatar Jun 21 '24 13:06 andreaTP

This also happens to be resolved with https://github.com/microsoft/kiota/pull/5657

andrueastman avatar Oct 22 '24 11:10 andrueastman