Puce
Puce

Reputation: 38142

SpringDoc + JsonSubTypes: no oneOf schema generated for intermediate class

We have a class hierarchy like the following:

BaseClass:

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Getter;
import lombok.Setter;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "type"
)
@JsonSubTypes({
        @JsonSubTypes.Type(value = Child1Class.class, name = "Child1Class"),
        @JsonSubTypes.Type(value = Child2Class.class, name = "Child2Class"),
        @JsonSubTypes.Type(value = Child3Class.class, name = "Child3Class")
})
@Getter
@Setter
public abstract class BaseClass {

    private String type = getClass().getSimpleName();

}

IntermediateClass extending BaseClass:

import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;

@JsonPropertyOrder({"type", "foo"})
@Getter
@Setter
public abstract class IntermediateClass extends BaseClass {

}

Child1Class extending IntermediateClass:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class Child1Class extends IntermediateClass {

    private String foo;

}

Child2Class extending IntermediateClass as well:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
public class Child2Class extends IntermediateClass {

    private Integer foo;

}

Child3Class extending BaseClass directly:

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonInclude(Include.NON_NULL)
@JsonPropertyOrder({"type", "bar"})
public class Child3Class extends BaseClass {

    private String bar;

}

The Spring MVC controller (Spring Boot v2.5.2 application) looks like this:

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("SSCCEController")
@RequestMapping(value = {"/api/sscce/tests"}, produces = MediaType.APPLICATION_JSON_VALUE)
public class SSCCEController {

    @GetMapping(path = "/base")
    public BaseClass getBaseTest() {
        return new Child1Class();
    }

    @GetMapping(path = "/intermediate")
    public IntermediateClass getIntermediateTest() {
        return new Child1Class();
    }

}

While for Jackson everything is fine, SpringDoc generates the following OpenAPI Spec:

{
  "openapi": "3.0.1",
  "info": {
    "title": "SSCCE - oneOf inheritance"
  },
  "servers": [
    {
      "description": "Generated server url",
      "url": "http://localhost"
    }
  ],
  "paths": {
    "/api/sscce/tests/base": {
      "get": {
        "operationId": "getBaseTest",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Child1Class"
                    },
                    {
                      "$ref": "#/components/schemas/Child2Class"
                    },
                    {
                      "$ref": "#/components/schemas/Child3Class"
                    }
                  ]
                }
              }
            },
            "description": "OK"
          }
        },
        "tags": [
          "sscce-controller"
        ]
      }
    },
    "/api/sscce/tests/intermediate": {
      "get": {
        "operationId": "getIntermediateTest",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntermediateClass"
                }
              }
            },
            "description": "OK"
          }
        },
        "tags": [
          "sscce-controller"
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "BaseClass": {
        "type": "object",
        "discriminator": {
          "propertyName": "type"
        },
        "properties": {
          "type": {
            "type": "string"
          }
        }
      },
      "Child1Class": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseClass"
          },
          {
            "type": "object",
            "properties": {
              "foo": {
                "type": "string"
              }
            }
          }
        ]
      },
      "Child2Class": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseClass"
          },
          {
            "type": "object",
            "properties": {
              "foo": {
                "type": "integer",
                "format": "int32"
              }
            }
          }
        ]
      },
      "Child3Class": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/BaseClass"
          },
          {
            "type": "object",
            "properties": {
              "bar": {
                "type": "string"
              }
            }
          }
        ]
      },
      "IntermediateClass": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string"
          }
        }
      }
    }
  }
}

So for /api/sscce/tests/base the response gets generated as oneOf as expected.

How can we make it also generate oneOf for /api/sscce/tests/intermediate?

Also in the allOf section of Child1Class and Child2Class the IntermediateClass should be referenced, not BaseClass! (I tried and added a property to IntermediateClass but nothing changed.)

Upvotes: 2

Views: 3061

Answers (1)

Puce
Puce

Reputation: 38142

I finally found a solution for this issue: add an @Schema-annotation for the IntermediateClass.

@Schema(
        type = "object",
        title = "IntermediateClass",
        subTypes = {Child1Class.class, Child2Class.class}
)
@JsonPropertyOrder({"type", "foo"})
public abstract class IntermediateClass extends BaseClass {

}

Upvotes: 5

Related Questions