Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inheritance + Swagger UI + Model Example help #1581

Closed
wesley-pattison opened this issue Sep 6, 2018 · 16 comments
Closed

Inheritance + Swagger UI + Model Example help #1581

wesley-pattison opened this issue Sep 6, 2018 · 16 comments

Comments

@wesley-pattison
Copy link

wesley-pattison commented Sep 6, 2018

Hi,

When using Swagger UI I am unable to get the Example Value to properly generate an example that contains the derived classes in the example.

    [JsonConverter(typeof(EntityCustomJsonConverter))]
    [KnownType(typeof(CompanyRequestModel))]
    [KnownType(typeof(PersonRequestModel))]
    public abstract class PartyBaseRequestModel : BaseObjectRequestModel
    {
        public EntityEnums.Parties.Role[] Roles { get; set; }
    }

This BaseEntityCustomJsonConverter is used on a custom ModelBinding for my incoming request to determine (from the EntityType property we ask in our request) which Object to case the jObject too.

public sealed class EntityCustomJsonConverter : BaseEntityCustomJsonConverter<BaseObjectRequestModel>
    {
        private const string EntityTypeKey = "EntityType";

        protected override BaseObjectRequestModel Create(Type objectType, JObject jsonObject)
        {
            if (!jsonObject.TryGetValue(EntityTypeKey, StringComparison.InvariantCultureIgnoreCase, out var entityType))
            {
                return default;
            }

            switch (entityType.ToString().ToLowerInvariant())
            {
                case EntityTypes.Parties.Person:
                    return jsonObject.ToObject<PersonRequestModel>();
                case EntityTypes.Parties.Company:
                    return jsonObject.ToObject<CompanyRequestModel>();
                default:
                    throw new NotSupportedException();
            }
        }
    }
    {
        protected abstract T Create(Type objectType, JObject jsonObject);
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var jsonObject = JObject.Load(reader);
            var target = Create(objectType, jsonObject);
            if (target != null)
            {
                serializer.Populate(jsonObject.CreateReader(), target);
            }

            return target;
        }

        public override bool CanConvert(Type objectType)
        {
            return typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo());
        }
    }

Image of partial Model Example:
image

Swagger UI see's my inherited class in the Model's according:
image

image

What I would actually like to see in Swagger UI is this: (This image was taken from a YAML file created by me for another API that we will create, hence the different properties - What I like achieve does not change though)
image

Notice how "nodes" property contains an AnyOf with a list of models. How can I achieve this through code?

@wesley-pattison
Copy link
Author

I am thinking either a ISchemaProcessor or IDocumentProcessor is needed but I could not find clear examples or help on how to achieve this.

@RicoSuter
Copy link
Owner

@wesley-pattison
Copy link
Author

I've taken a look at this. Even with the example given on the page; I don't get my expected outcome.

Give me a couple minutes and I'll show you.

@wesley-pattison
Copy link
Author

wesley-pattison commented Sep 6, 2018

As you see, the example value only contains the properties for the base class:

image

image

        [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        // GET api/values/5
        [HttpGet("{id}")]
        public IActionResult Get(Container incoming)
        {
            return Ok();
        }
    }

    public class Container
    {
        public Animal[] Animals { get; set; }
    }

    [JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
    [KnownType(typeof(Dog))]
    [KnownType(typeof(Cat))]
    public class Animal
    {
        public string Foo { get; set; }
    }

    public class Dog : Animal
    {
        public string Bar { get; set; }
    }

    public class Cat : Animal
    {
        public string Meow { get; set; }
    }

Startup.cs

            app.UseSwaggerUi3WithApiExplorer(settings =>
            {
                settings.GeneratorSettings.DefaultEnumHandling = EnumHandling.String;
                settings.GeneratorSettings.AllowReferencesWithProperties = true;
                settings.GeneratorSettings.Title = "Screening API v1";
                settings.SwaggerRoute = "/swagger/v1/swagger.json";
                settings.SwaggerUiRoute = "/swagger";
            });

@wesley-pattison
Copy link
Author

wesley-pattison commented Sep 6, 2018

I've updated my code example. The parameter on the controller is now set to Container, and I've made sure that Animal is now an array. I would expect to see the result in my previous image about anyOf.

swagger.json:

{
  "x-generator": "NSwag v11.19.0.0 (NJsonSchema v9.10.72.0 (Newtonsoft.Json v11.0.0.0))",
  "swagger": "2.0",
  "info": {
    "title": "Screening API v1",
    "version": "1.0.0"
  },
  "host": "localhost:44379",
  "schemes": [
    "https"
  ],
  "consumes": [
    "application/json-patch+json",
    "application/json",
    "text/json",
    "application/*+json"
  ],
  "paths": {
    "/api/Values/{id}": {
      "get": {
        "tags": [
          "Values"
        ],
        "operationId": "Values_Get",
        "consumes": [
          "application/json-patch+json",
          "application/json",
          "text/json",
          "application/*+json"
        ],
        "parameters": [
          {
            "name": "incoming",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/Container"
            },
            "x-nullable": true
          },
          {
            "type": "string",
            "name": "id",
            "in": "path",
            "required": true,
            "x-nullable": false
          }
        ],
        "responses": {
          "200": {
            "x-nullable": true,
            "description": "",
            "schema": {
              "type": "file"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Container": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "Animals": {
          "type": "array",
          "items": {
            "$ref": "#/definitions/Animal"
          }
        }
      }
    },
    "Animal": {
      "type": "object",
      "discriminator": "discriminator",
      "additionalProperties": false,
      "required": [
        "discriminator"
      ],
      "properties": {
        "Foo": {
          "type": "string"
        },
        "discriminator": {
          "type": "string"
        }
      }
    },
    "Dog": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "Bar": {
          "type": "string"
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/Animal"
        }
      ]
    },
    "Cat": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "Meow": {
          "type": "string"
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/Animal"
        }
      ]
    }
  }
}

@RicoSuter
Copy link
Owner

Related PR: RicoSuter/NJsonSchema#733

@wesley-pattison
Copy link
Author

wesley-pattison commented Sep 6, 2018

Related PR: RicoSuter/NJsonSchema#733

Edit: I think I can make something work after looking into NJsonSchema.Tests.Generation.InheritanceTests specifically with the FlattenInheritanceHierarchy set to true.

What I believe I need to achieve is use a processor to read the EntityType property if it exists, resolve the type by the value of the string, then generate a schema based off that type that contain the flatten properties from the inherited object. Finally, the schema then needs to be replaced.

Does all this seem achievable? What processor should I use - IDocumentProcessor or ISchemaProcessor?

@wesley-pattison
Copy link
Author

So I'm making progress thanks for the selection of tests you have on Inheritance! 👍

In my UI I now see the following:

JSON representation shows, but is missing information I assume :)
image

Model example doesn't show the list of supported derived types:
image

Custom Schema Processor

            public async Task ProcessAsync(SchemaProcessorContext context)
            {
                if (context.Type.Name is nameof(PartyBaseRequestModel))
                {;
                    foreach (KnownTypeAttribute attribute in context.Type.GetCustomAttributes(typeof(KnownTypeAttribute), true))
                    {
                        var schema = await JsonSchema4.FromTypeAsync(attribute.Type, new JsonSchemaGeneratorSettings
                        {
                            FlattenInheritanceHierarchy = true
                        });
                        context.Schema.AnyOf.Add(schema);
                    }
                    //var json = context.Schema.ToJson();
                }
            }

@RicoSuter
Copy link
Owner

Just FYI: AnyOf inheritance is not (yet) supported by the generator:
RicoSuter/NJsonSchema#13

Also the Swagger spec only describes allOf inheritance.

You shouldnt use JsonSchema4.FromTypeAsync in a schema processor but context.SchemaGenerator, so that schemas are correctly added to definitions etc. but then you can't just change the settings (e.g. FlattenInheritanceHierarchy).

It seems that you want to completely customize inheritance handling, maybe you need an own implementation of JsonSchemaGenerator with some overrides...

@wesley-pattison
Copy link
Author

wesley-pattison commented Sep 7, 2018

So, after a bit of toying around, I was able to achieve one thing:

Having JsonSchema validate correctly based off my complex request. See code below:

            if (context.Type.Name is nameof(PartyBaseRequestModel) || context.Type.Name is nameof(BaseObjectRequestModel))
            {
                var attributes = context.Type.GetCustomAttributes(typeof(KnownTypeAttribute), true) as Attribute[];
                foreach (var attribute1 in attributes)
                {
                    var attribute = (KnownTypeAttribute) attribute1;
                    var schema = await context.Generator.GenerateWithReferenceAndNullabilityAsync<JsonSchema4>(attribute.Type, attributes, context.Resolver,
                        async (p, s) => { p.AdditionalPropertiesSchema = s; });
                    context.Schema.Definitions.Add(attribute.Type.Name, schema);
                }

                context.Schema.AllowAdditionalProperties = true;
                context.Schema.AllowAdditionalItems = true;
                context.Schema.Properties.Clear();
            }

Now, how do I proceed with possibly making SwaggerUI generate the proper examples? Would this custom JsonSchemaGenerator be the correct approach?

image

@RicoSuter
Copy link
Owner

I think you shouldnt use GenerateWithReferenceAndNullabilityAsync when you add the schema to definitions (in definitions they need to be inline, not nullable or referenced)

@wesley-pattison
Copy link
Author

We're just about there!

image

if (context.Type.Name is nameof(PartyBaseRequestModel) || context.Type.Name is nameof(BaseObjectRequestModel))
            {
                var attributes = context.Type.GetCustomAttributes(typeof(KnownTypeAttribute), true) as Attribute[];
                foreach (var attribute1 in attributes)
                {
                    var attribute = (KnownTypeAttribute) attribute1;
                    var schema = await context.Generator.GenerateWithReferenceAndNullabilityAsync<JsonSchema4>(attribute.Type, attributes, context.Resolver,
                        async (p, s) =>
                        {
                            p.AllowAdditionalProperties = true;
                            p.AdditionalPropertiesSchema = s.AdditionalPropertiesSchema;
                        });
                    schema.AllowAdditionalProperties = true;
                    context.Schema.AnyOf.Add(schema);
                    //context.Schema.Definitions.Add(attribute.Type.Name, schema);
                }

                context.Schema.AllowAdditionalProperties = true;
            }

However, I'm still missing the AnyOf or OneOf implementation in the Model example view:

image

What needs to be implemented for this?

@RicoSuter
Copy link
Owner

I dont know if Swagger UI even supports anyOf or oneOf. Is that the case?

@wesley-pattison
Copy link
Author

image

It should, this was another request I manually constructed in YAML and pasted it in editor.swagger.io

@RicoSuter
Copy link
Owner

Why are you modifying

p.AllowAdditionalProperties = true;
p.AdditionalPropertiesSchema = s.AdditionalPropertiesSchema;
schema.AllowAdditionalProperties = true;

Try to use

GenerateWithReferenceAsync

And ensure that the AnyOf in the JSON contains only $refs

@wesley-pattison
Copy link
Author

Well, I feel a bit silly. I never changed my SchemaType to OpenApi 3.0 - Only this supports the anyOf keyword.

With this code:

            if (context.Type.Name is nameof(PartyBaseRequestModel) || context.Type.Name is nameof(BaseObjectRequestModel))
            {
                var attributes = context.Type.GetCustomAttributes(typeof(KnownTypeAttribute), true) as Attribute[];
                foreach (var attribute1 in attributes)
                {
                    var attribute = (KnownTypeAttribute) attribute1;
                    var schema = await context.Generator.GenerateWithReferenceAsync<JsonSchema4>(attribute.Type, attributes, context.Resolver,
                        async (p, s) => {});
                    context.Schema.AnyOf.Add(schema);
                }

                context.Schema.Properties.Clear();
                context.Schema.AllowAdditionalProperties = true;
            }

and the settings:

                settings.GeneratorSettings.FlattenInheritanceHierarchy = true;
                settings.GeneratorSettings.SchemaType = SchemaType.OpenApi3;
                settings.GeneratorSettings.SchemaProcessors.Add(new KnownTypeSchemaProcessor());

I was able to achieve:
image

Thanks for support! 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants