RicoSuter / NSwag

The Swagger/OpenAPI toolchain for .NET, ASP.NET Core and TypeScript.
http://NSwag.org
MIT License
6.69k stars 1.24k forks source link

Support discriminator with options that point to the same reference #3356

Open JoostvdB94 opened 3 years ago

JoostvdB94 commented 3 years ago

Today when I was trying to generate a C# client using NSwagStudio I came across the following error:

System.InvalidOperationException: Error while rendering Liquid template CSharp/Class: 
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.InvalidOperationException: Sequence contains more than one matching element

Runtime: NetCore31
   at System.Linq.ThrowHelper.ThrowMoreThanOneMatchException()
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.DerivedClassModel..ctor(String typeName, JsonSchema schema, OpenApiDiscriminator discriminator, TypeResolverBase resolver)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.<get_DerivedClasses>b__16_0(KeyValuePair`2 p)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.get_DerivedClasses()
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
   at System.Reflection.PropertyInfo.GetValue(Object obj)
   at NJsonSchema.CodeGeneration.LiquidProxyHash.GetValue(String key)
   at DotLiquid.Hash.System.Collections.IDictionary.get_Item(Object key)
   at DotLiquid.Context.LookupAndEvaluate(Object obj, Object key)
   at DotLiquid.Context.FindVariable(String key)
   at DotLiquid.Context.Variable(String markup, Boolean notifyNotFound)
   at DotLiquid.Context.Resolve(String key, Boolean notifyNotFound)
   at DotLiquid.Context.get_Item(String key, Boolean notifyNotFound)
   at DotLiquid.Tags.For.Render(Context context, TextWriter result)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
--- End of stack trace from previous location where exception was thrown ---
   at DotLiquid.Context.HandleError(Exception ex)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
   at DotLiquid.Tags.If.<>c__DisplayClass11_0.<Render>b__0()
   at DotLiquid.Context.Stack(Hash newScope, Action callback)
   at DotLiquid.Context.Stack(Action callback)
   at DotLiquid.Tags.If.Render(Context context, TextWriter result)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
--- End of stack trace from previous location where exception was thrown ---
   at DotLiquid.Context.HandleError(Exception ex)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
   at DotLiquid.Block.Render(Context context, TextWriter result)
   at DotLiquid.Document.Render(Context context, TextWriter result)
   at DotLiquid.Template.RenderInternal(TextWriter result, RenderParameters parameters)
   at DotLiquid.Template.Render(TextWriter writer, RenderParameters parameters)
   at DotLiquid.Template.Render(RenderParameters parameters)
   at NJsonSchema.CodeGeneration.DefaultTemplateFactory.LiquidTemplate.Render()
 ---> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.InvalidOperationException: Sequence contains more than one matching element
   at System.Linq.ThrowHelper.ThrowMoreThanOneMatchException()
   at System.Linq.Enumerable.SingleOrDefault[TSource](IEnumerable`1 source, Func`2 predicate)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.DerivedClassModel..ctor(String typeName, JsonSchema schema, OpenApiDiscriminator discriminator, TypeResolverBase resolver)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.<get_DerivedClasses>b__16_0(KeyValuePair`2 p)
   at System.Linq.Enumerable.SelectEnumerableIterator`2.ToList()
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at NJsonSchema.CodeGeneration.Models.ClassTemplateModelBase.get_DerivedClasses()
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, BindingFlags invokeAttr, Binder binder, Object[] index, CultureInfo culture)
   at System.Reflection.RuntimePropertyInfo.GetValue(Object obj, Object[] index)
   at System.Reflection.PropertyInfo.GetValue(Object obj)
   at NJsonSchema.CodeGeneration.LiquidProxyHash.GetValue(String key)
   at DotLiquid.Hash.System.Collections.IDictionary.get_Item(Object key)
   at DotLiquid.Context.LookupAndEvaluate(Object obj, Object key)
   at DotLiquid.Context.FindVariable(String key)
   at DotLiquid.Context.Variable(String markup, Boolean notifyNotFound)
   at DotLiquid.Context.Resolve(String key, Boolean notifyNotFound)
   at DotLiquid.Context.get_Item(String key, Boolean notifyNotFound)
   at DotLiquid.Tags.For.Render(Context context, TextWriter result)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
--- End of stack trace from previous location where exception was thrown ---
   at DotLiquid.Context.HandleError(Exception ex)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
   at DotLiquid.Tags.If.<>c__DisplayClass11_0.<Render>b__0()
   at DotLiquid.Context.Stack(Hash newScope, Action callback)
   at DotLiquid.Context.Stack(Action callback)
   at DotLiquid.Tags.If.Render(Context context, TextWriter result)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
--- End of stack trace from previous location where exception was thrown ---
   at DotLiquid.Context.HandleError(Exception ex)
   at DotLiquid.Block.RenderAll(List`1 list, Context context, TextWriter result)
   at DotLiquid.Block.Render(Context context, TextWriter result)
   at DotLiquid.Document.Render(Context context, TextWriter result)
   at DotLiquid.Template.RenderInternal(TextWriter result, RenderParameters parameters)
   at DotLiquid.Template.Render(TextWriter writer, RenderParameters parameters)
   at DotLiquid.Template.Render(RenderParameters parameters)
   at NJsonSchema.CodeGeneration.DefaultTemplateFactory.LiquidTemplate.Render()
   --- End of inner exception stack trace ---
   at NJsonSchema.CodeGeneration.DefaultTemplateFactory.LiquidTemplate.Render()
   at NJsonSchema.CodeGeneration.CodeArtifact..ctor(String typeName, String baseTypeName, CodeArtifactType type, CodeArtifactLanguage language, CodeArtifactCategory category, ITemplate template)
   at NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateClass(JsonSchema schema, String typeName)
   at NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateType(JsonSchema schema, String typeNameHint)
   at NJsonSchema.CodeGeneration.GeneratorBase.GenerateTypes()
   at NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateTypes()
   at NSwag.CodeGeneration.CSharp.CSharpGeneratorBase.GenerateDtoTypes() in C:\projects\nswag\src\NSwag.CodeGeneration.CSharp\CSharpGeneratorBase.cs:line 106
   at NSwag.CodeGeneration.ClientGeneratorBase`3.GenerateFile(ClientGeneratorOutputType outputType) in C:\projects\nswag\src\NSwag.CodeGeneration\ClientGeneratorBase.cs:line 75
   at NSwag.Commands.CodeGeneration.SwaggerToCSharpClientCommand.<RunAsync>b__95_0() in C:\projects\nswag\src\NSwag.Commands\Commands\CodeGeneration\OpenApiToCSharpClientCommand.cs:line 273
   at NSwag.Commands.CodeGeneration.SwaggerToCSharpClientCommand.RunAsync() in C:\projects\nswag\src\NSwag.Commands\Commands\CodeGeneration\OpenApiToCSharpClientCommand.cs:line 259
   at NSwag.Commands.CodeGeneration.SwaggerToCSharpClientCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in C:\projects\nswag\src\NSwag.Commands\Commands\CodeGeneration\OpenApiToCSharpClientCommand.cs:line 248
   at NSwag.Commands.NSwagDocument.ExecuteAsync() in C:\projects\nswag\src\NSwag.Commands\NSwagDocument.cs:line 85
   at NSwag.Commands.Document.ExecuteDocumentCommand.ExecuteDocumentAsync(IConsoleHost host, String filePath) in C:\projects\nswag\src\NSwag.Commands\Commands\Document\ExecuteDocumentCommand.cs:line 86
   at NSwag.Commands.Document.ExecuteDocumentCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in C:\projects\nswag\src\NSwag.Commands\Commands\Document\ExecuteDocumentCommand.cs:line 32
   at NConsole.CommandLineProcessor.ProcessSingleAsync(String[] args, Object input)
   at NConsole.CommandLineProcessor.ProcessAsync(String[] args, Object input)
   at NConsole.CommandLineProcessor.Process(String[] args, Object input)
   at NSwag.Commands.NSwagCommandProcessor.Process(String[] args) in C:\projects\nswag\src\NSwag.Commands\NSwagCommandProcessor.cs:line 56

After a lot of attempts I found that the following snippet was causing an issue:

components:
  schemas:
    Vehicle:
      type: object
    PassengerVehicle:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          required: 
             - numberOfSeats
          properties:
            numberOfSeats:
              type: integer
          discriminator:
            propertyName: numberOfSeats
            mapping:
              "1": "#/components/schemas/Bike"
              "2": "#/components/schemas/SportsCar"
              "3": "#/components/schemas/Car"
              "5": "#/components/schemas/Car"
              "6": "#/components/schemas/MiniVan"
              "10": "#/components/schemas/Bus"
              "18": "#/components/schemas/Bus"
              "20": "#/components/schemas/Bus"

after I removed duplicatie values in the mapping, the generation succeeded without errors. (snippet below)

components:
  schemas:
    Vehicle:
      type: object
    PassengerVehicle:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          required: 
             - numberOfSeats
          properties:
            numberOfSeats:
              type: integer
          discriminator:
            propertyName: numberOfSeats
            mapping:
              "1": "#/components/schemas/Bike"
              "2": "#/components/schemas/SportsCar"
              "5": "#/components/schemas/Car"
              "6": "#/components/schemas/MiniVan"
              "18": "#/components/schemas/Bus"

I do not remember reading that duplicate values cannot exist in the OAS3 specification. That's why I think it is a bug.

Note: the examples above are truncated. The referenced schemas do exist

JoostvdB94 commented 3 years ago

Another error must have occured. I stripped down the spec to everything but the schema's and it worked fine.

Unfortunately I cannot (yet) share the complete spec as it contains classified information.

JoostvdB94 commented 3 years ago

Closing it for now as I assume the problem is elsewhere, and cannot be identified without the spec itself.

JoostvdB94 commented 3 years ago

I managed to reproduce the issue using the following configuration:

openapi: '3.0.2'
info:
  title: API Title
  version: '1.0'
servers:
  - url: https://api.server.test/v1
paths:
  /test:
    get:
      responses:
        '200':
          description: OK
components:
  schemas:
    Vehicle:
      type: object
    PassengerVehicle:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          required: 
             - numberOfSeats
          properties:
            numberOfSeats:
              type: integer
          discriminator:
            propertyName: numberOfSeats
            mapping:
              "1": "#/components/schemas/Bike"
              "2": "#/components/schemas/SportsCar"
              "3": "#/components/schemas/Car"
              "5": "#/components/schemas/Car"
              "6": "#/components/schemas/MiniVan"
              "10": "#/components/schemas/Bus"
              "18": "#/components/schemas/Bus"
              "20": "#/components/schemas/Bus"
    Bus:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    Car:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    SportsCar:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    MiniVan:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    Bike:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object

When I use the sample spec above, the result is the error posted in my first message. If it is altered to only use a single mapping entry per type as illustrated in the following snippet, the error dissapears.

openapi: '3.0.2'
info:
  title: API Title
  version: '1.0'
servers:
  - url: https://api.server.test/v1
paths:
  /test:
    get:
      responses:
        '200':
          description: OK
components:
  schemas:
    Vehicle:
      type: object
    PassengerVehicle:
      allOf:
        - $ref: "#/components/schemas/Vehicle"
        - type: object
          required: 
             - numberOfSeats
          properties:
            numberOfSeats:
              type: integer
          discriminator:
            propertyName: numberOfSeats
            mapping:
              "1": "#/components/schemas/Bike"
              "2": "#/components/schemas/SportsCar"
              "3": "#/components/schemas/Car"
              "6": "#/components/schemas/MiniVan"
              "10": "#/components/schemas/Bus"
    Bus:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    Car:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    SportsCar:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    MiniVan:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object
    Bike:
      allOf:
        - $ref: "#/components/schemas/PassengerVehicle"
        - type: object

Also, when the allOf-reference in the different PassengerVehicle's are changed to for instance Vehicle instead of PassengerVehicle, the code generation works as expected.

Any ideas on how to fix this?

TSteschulat commented 3 years ago

Same Problem here. Any news on it?

JoostvdB94 commented 3 years ago

@RicoSuter Do you perhaps have any idea where this issue may originate? If so, i'm happy to investigate further and provide a fix

Looking at the stacktrace, this may also be an issue in NJsonSchema (JsonSchema.CodeGeneration.Models.ClassTemplateModelBase.get_DerivedClasses())