RicoSuter / NJsonSchema

JSON Schema reader, generator and validator for .NET
http://NJsonSchema.org
MIT License
1.4k stars 534 forks source link

Client generation error with NJsonSchema 11 Preview with NetTopologySuite geometries #1657

Closed rootix closed 9 months ago

rootix commented 10 months ago

I'm in the process of upgrading a .NET 7 based solution which uses Nswag assembly scanning to .NET 8 with the .csproj approach.

I get an error with our use of NetTopologySuite geometries on the upgraded solution:

dotnet "C:\Users\pm\.nuget\packages\nswag.msbuild\14.0.0-preview012\buildTransitive\../tools/Net80/dotnet-nswag.dll" run nswag.json /variables:Configuration=Debug
NSwag command line tool for .NET Core Net80, toolchain v14.0.0.0 (NJsonSchema v11.0.0.0 (Newtonsoft.Json v13.0.0.0))
Visit http://NSwag.org for more information.
NSwag bin directory: C:\Users\pm\.nuget\packages\nswag.msbuild\14.0.0-preview012\tools\Net80

Executing file 'nswag.json' with variables 'Configuration=Debug'...
Launcher directory: C:\Users\pm\.nuget\packages\nswag.msbuild\14.0.0-preview012\tools\Net80
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.InvalidOperationException: The JSON property 'item' is defined multiple times on type 'NetTopologySuite.Geometries.Coordinate'.
   at NJsonSchema.Generation.SystemTextJsonReflectionService.GenerateProperties(JsonSchema schema, ContextualType contextualType, SystemTextJsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.ReflectionServiceBase`1.NJsonSchema.Generation.IReflectionService.GenerateProperties(JsonSchema schema, ContextualType contextualType, JsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateObject(JsonSchema schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateObject(JsonSchema schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](TSchemaType schema, ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](Type type, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate(Type type, JsonSchemaResolver schemaResolver)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateObject(JsonSchema schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](TSchemaType schema, ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateArray[TSchemaType](TSchemaType schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](TSchemaType schema, ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NJsonSchema.Generation.JsonSchemaGenerator.AddProperty(JsonSchema parentSchema, ContextualAccessorInfo property, JsonTypeDescription propertyTypeDescription, String propertyName, Attribute requiredAttribute, Boolean hasRequiredAttribute, Boolean isNullable, Object defaultValue, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.SystemTextJsonReflectionService.GenerateProperties(JsonSchema schema, ContextualType contextualType, SystemTextJsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.ReflectionServiceBase`1.NJsonSchema.Generation.IReflectionService.GenerateProperties(JsonSchema schema, ContextualType contextualType, JsonSchemaGeneratorSettings settings, JsonSchemaGenerator schemaGenerator, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateInheritance(ContextualType type, JsonSchema schema, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateObject(JsonSchema schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateObject(JsonSchema schema, JsonTypeDescription typeDescription, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](TSchemaType schema, ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.Generate[TSchemaType](ContextualType contextualType, JsonSchemaResolver schemaResolver)
   at NJsonSchema.Generation.JsonSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NSwag.Generation.OpenApiSchemaGenerator.GenerateWithReferenceAndNullability[TSchemaType](ContextualType contextualType, Boolean isNullable, JsonSchemaResolver schemaResolver, Action`2 transformation)
   at NSwag.Generation.AspNetCore.Processors.OperationResponseProcessor.Process(OperationProcessorContext operationProcessorContext)
   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.RunOperationProcessors(OpenApiDocument document, ApiDescription apiDescription, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List`1 allOperations, OpenApiDocumentGenerator generator, OpenApiSchemaResolver schemaResolver)
   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.AddOperationDescriptionsToDocument(OpenApiDocument document, Type controllerType, List`1 operations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver)
   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateApiGroups(OpenApiDocumentGenerator generator, OpenApiDocument document, IGrouping`2[] apiGroups, OpenApiSchemaResolver schemaResolver)
   at NSwag.Generation.AspNetCore.AspNetCoreOpenApiDocumentGenerator.GenerateAsync(ApiDescriptionGroupCollection apiDescriptionGroups)
   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.GenerateDocumentWithDocumentProviderAsync(IServiceProvider serviceProvider) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 245
   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.GenerateDocumentAsync(IServiceProvider serviceProvider, String currentWorkingDirectory) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 239
   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiGeneratorCommandEntryPoint.Process(String commandContent, String outputFile, String applicationName) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiGeneratorCommandEntryPoint.cs:line 29
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   --- End of inner exception stack trace ---
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at NSwag.AspNetCore.Launcher.Program.Main(String[] args) in /_/src/NSwag.AspNetCore.Launcher/Program.cs:line 132
System.InvalidOperationException: Swagger generation failed with non-zero exit code '1'.
   at NSwag.Commands.Generation.AspNetCore.AspNetCoreToOpenApiCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in /_/src/NSwag.Commands/Commands/Generation/AspNetCore/AspNetCoreToOpenApiCommand.cs:line 195
   at NSwag.Commands.NSwagDocumentBase.GenerateSwaggerDocumentAsync() in /_/src/NSwag.Commands/NSwagDocumentBase.cs:line 270
   at NSwag.Commands.NSwagDocument.ExecuteAsync() in /_/src/NSwag.Commands/NSwagDocument.cs:line 67
   at NSwag.Commands.Document.ExecuteDocumentCommand.ExecuteDocumentAsync(IConsoleHost host, String filePath) in /_/src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 76
   at NSwag.Commands.Document.ExecuteDocumentCommand.RunAsync(CommandLineProcessor processor, IConsoleHost host) in /_/src/NSwag.Commands/Commands/Document/ExecuteDocumentCommand.cs:line 33
   at NConsole.CommandLineProcessor.ProcessSingleAsync(String[] args, Object input)
   at NConsole.CommandLineProcessor.ProcessAsync(String[] args, Object input)
   at NSwag.Commands.NSwagCommandProcessor.ProcessAsync(String[] args) in /_/src/NSwag.Commands/NSwagCommandProcessor.cs:line 62

For testing purposes, i switched the .NET 7 version over to the .csproj approach where it worked.

I prepared a minimal sample repository with 2 projects (MsBuild AfterBuildTarget):

https://github.com/rootix/NJsonSchemaIssue

Everything except the updated .NET version and the updated Packages (Nswag 14 Preview 12) is the same.

Am i missing something? I tried to add "Coordinate" to the ExcludedTypeNames, but with no luck. And it worked in the older version anyway.

Thanks for the help!

RobSlgm commented 10 months ago

Using the AddOpenApiDocument call to set a flat hierarchy solves the first issue and allows compilation again.

namespace Net80NJsonSchema11;

using System.Text.Json;
using NJsonSchema;
using NJsonSchema.Generation;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddOpenApiDocument(c =>
        {
            c.SchemaSettings = new SystemTextJsonSchemaGeneratorSettings
            {
                SchemaType = SchemaType.OpenApi3,
                FlattenInheritanceHierarchy = true,
            };
        });
        builder.Services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new NetTopologySuite.IO.Converters.GeoJsonConverterFactory()));
        // builder.Services.AddSwaggerDocument();

        var app = builder.Build();
        app.MapControllers();

        app.Run();
    }
}

If you exclude further types, e.g "excludedTypeNames" in nswag.json: "LazyOfPrecisionModel", "SortIndexValue" the generated api.ts is basically the same as with sdk 7.

But in both version (7 / 8) the OpenApi definition while using NetTopologySuite NetTopologySuite is misleading or wrong, hence the reason why you exclude types or I use for very restricted use cases ITypeMapper.

rootix commented 10 months ago

Thanks for your investigation! The generation works indeed on my way more complex case.

But i can't handle the flat hierarchy in our project. I have different types where inheritance is essential in the client code and additionally i use the JsonInheritanceConverter which completly breaks by setting this flag. There is no discriminator or mapping code for child classes anymore.

I understand that NetTopology is not serializable. That is the reason why they provide a GeoJson package for conversion (Newtonsoft and System.Text.Json) that we use on the API at runtime. I saw that you configured the SchemaSettings with SystemTextJsonSchemaGeneratorSettings where i can define custom json serializer options and had hope that i can register the geometry factory there to make them serializable. But i still get the same Exception as reported.

Since this worked with NJsonSchema 10 (or because of .NET 7?), is this a problem that can (and should) be solved in NJsonSchema 11 to get it working again or am I out of luck here?

RobSlgm commented 10 months ago

For a restricted subset of GeoJson I found the use of ITypeMapper the most convenient solution. In your example add the typemapper for Point at startup. FlattenInheritanceHierarchy is no longer necessary.

namespace Net80NJsonSchema11;
public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddOpenApiDocument(c =>
        {
            c.SchemaSettings.TypeMappers.Add(new PointTypeMapper());
        });
        builder.Services.AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new NetTopologySuite.IO.Converters.GeoJsonConverterFactory()));

        var app = builder.Build();
        app.UseOpenApi();
        app.MapControllers();
        app.UseSwaggerUi(options =>
        {
            options.Path = "/openapi";
        });

        app.Run();
    }
}

Typemapper for GeoJson Point type

using NetTopologySuite.Geometries;
using NJsonSchema;
using NJsonSchema.Generation.TypeMappers;

namespace Net80NJsonSchema11;

// see also for specification https://app.swaggerhub.com/apis/OlivierMartineau/GeoJSON/1.0.0#
class PointTypeMapper : ITypeMapper
{
    public void GenerateSchema(JsonSchema schema, TypeMapperContext context)
    {
        if (context.Type == typeof(Point))
        {
            var hasSchema = context.JsonSchemaResolver.HasSchema(typeof(Point), false);
            schema.Type = JsonObjectType.Object;
            schema.Description = "GeoJSON Point";
            schema.Properties.Add("type", new JsonSchemaProperty
            {
                Type = JsonObjectType.String,
                IsRequired = true,
                Example = "Point",
            });
            schema.Properties.Add("coordinates", new JsonSchemaProperty
            {
                Type = JsonObjectType.Array,
                Item = new JsonSchema
                {
                    Type = JsonObjectType.Number
                },
                IsRequired = true,
                Example = new double[] { 8.541694, 47.376888 },
                MinItems = 2,
                MaxItems = 3,
                Description = "A position is an array of numbers. There MUST be two or more elements. The first two elements are longitude and latitude, or easting and northing, precisely in that order and using decimal numbers. Altitude or elevation MAY be included as an optional third element."
            });
            if (!hasSchema)
            {
                context.JsonSchemaResolver.AddSchema(typeof(Point), false, schema);
            }

        }
    }

    public Type MappedType => typeof(Point);
    public bool UseReference => true;
}

The excludedTypeNames are no longer necessary. This approach gives you a correct JSON (by o.JsonSerializerOptions.Converters.Add(new NetTopologySuite.IO.Converters.GeoJsonConverterFactory())) and a API definition which is for this usecase correct.

Please use with caution, in the final production version I never exposed NetTopologySuite objects but copied the info to an own external representation.

rootix commented 9 months ago

Thanks @RobSlgm for your your example! I wrote type mappers for almost the complete spec even if i still have to exclude them on the client side since the use of geojson.js (And despite the generated open api spec looks the same as the reference, the client is generated differently, but this is another problem). But it makes the Swagger UI more useful.

I got things working for me and this issue is therefore resolved. I'm not sure whether something with the reflection stuff is broken or if this is an intended / not relevant behaviour tho.