Search code examples
swaggerswashbuckleopenapi-generatorswashbuckle.aspnetcoreswashbuckle.examples

Is it possible to generate referenced schema for oneof properties using Swashbuckle?


By default, Swashbuckle generates an inline schema for oneof properties(universeCategory) like below.

"Universe":{
  "type":"object",
  "properties":{
    "universeCategory":{
      "oneOf":[
        {
          "$ref":"#/components/schemas/FullUniverse"
        },
        {
          "$ref":"#/components/schemas/HalfUniverse"
        }
      ],
      "discriminator":{
        "propertyName":"source",
        "mapping":{
          "FullUniverse":"#/components/schemas/FullUniverse",
          "HalfUniverse":"#/components/schemas/HalfUniverse"
        }
      }
    }
  }
}

Is it possible to generate referenced schema like below by passing some configuration to Swashbuckle?

"Universe":{
  "type":"object",
  "properties":{
    "universe":{
      "$ref":"#/components/schemas/UniverseCategory"
    }
  },
  "additionalProperties":false
},
"UniverseCategory":{
  "oneOf":[
    {
      "$ref":"#/components/schemas/HalfUniverse"
    },
    {
      "$ref":"#/components/schemas/FullUniverse"
    }
  ],
  "discriminator":{
    "propertyName":"source",
    "mapping":{
      "HalfUniverse":"#/components/schemas/HalfUniverse",
      "FullUniverse":"#/components/schemas/FullUniverse"
    }
  }
}

Tools like Open API generator supports only the above format as of now. So, any workaround to generate referenced schema for oneof properties is appreciated.


Solution

  • we can add a custom filter to handle the generation of oneof schemas as a seperate schema and can add reference to the generated new schema.

    public class HandleOneOfPropertiesFilter : ISchemaFilter
    {
        public void Apply(OpenApiSchema schema, SchemaFilterContext context)
        {
            if (schema?.Properties == null || context.Type == null)
            {
                return;
            }
            
            var propertiesWithOneOfHandling = context.Type.GetProperties()
                .Where(t => t.GetCustomAttributes().Any(c => c.GetType() == typeof(HandleOneOfPropertiesAttribute)));
    
            foreach (var selectedProps in propertiesWithOneOfHandling)
            {
    
                foreach (var props in schema.Properties)
                {
                    if (selectedProps.Name.Equals(props.Key, StringComparison.InvariantCultureIgnoreCase))
                    {
                        var oneOfProperty = (HandleOneOfPropertiesAttribute)context.Type.GetProperty(selectedProps.Name)
                            .GetCustomAttribute(typeof(HandleOneOfPropertiesAttribute));
                        
                        var name = oneOfProperty.Prefix + selectedProps.Name;
    
                        if (props.Value.Type == "array")
                        {
                            // Handling array type differently
                            context.SchemaRepository.Schemas.Add(name, props.Value.Items);
                            
                            var newSchema = new OpenApiSchema();
                            newSchema.Type = "array";
                            newSchema.Items  = new OpenApiSchema
                            {
                                Reference = new OpenApiReference
                                {
                                    Id = name,
                                    Type = ReferenceType.Schema
                                }
                            };
                            context.SchemaRepository.Schemas.Add(name + "Array", newSchema);
                            props.Value.Reference = new OpenApiReference
                            {
                                Id = name + "Array",
                                Type = ReferenceType.Schema
                            };
                        }
                        else
                        {
                            context.SchemaRepository.Schemas.Add(name, props.Value);    
                            props.Value.Reference = new OpenApiReference
                            {
                                Id = name,
                                Type = ReferenceType.Schema
                            };
                        }
                    }
                }
            }
        }
    }
    

    Then we need to define an Attribute to identify on which properties we need to handle the one of generation, lets create the attribute

    public class HandleOneOfPropertiesAttribute : Attribute
    {
        public HandleOneOfPropertiesAttribute(string prefix)
        {
            Prefix = prefix;
        }
        
        public string Prefix { get; }
    }
    

    Then we need to use this attribute for the properties of model that are oneof type. In the below snippet, I'm using a prefix of "OneOfProp", so the new schema generated will have this prefix

    public class ModelClass
    {
       
        [HandleOneOfProperties("OneOfProp")]
        public Universe property1 { get; set; }
    
    
        [HandleOneOfProperties("OneOfProp")]
        public Galaxy property2 { get; set; }
    }
    

    Finally register this filter in the services

    services.AddSwaggerGen(c =>
     {
       c.SchemaFilter<HandleOneOfPropertiesFilter>();
     });