SpringDoc + JsonSubTypes:没有为中间 class 生成模式
SpringDoc + JsonSubTypes: no oneOf schema generated for intermediate class
我们有一个 class 层次结构,如下所示:
基类:
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();
}
中间类扩展基类:
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;
@JsonPropertyOrder({"type", "foo"})
@Getter
@Setter
public abstract class IntermediateClass extends BaseClass {
}
Child1Class 扩展 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 也扩展了 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 Child2Class extends IntermediateClass {
private Integer foo;
}
直接扩展 BaseClass 的 Child3Class:
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;
}
Spring MVC 控制器(Spring Boot v2.5.2 应用程序)如下所示:
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();
}
}
虽然 Jackson 一切正常,SpringDoc 生成了以下 OpenAPI 规范:
{
"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"
}
}
}
}
}
}
因此,对于 /api/sscce/tests/base
,响应将按预期生成为 oneOf
。
我们如何让它也为 /api/sscce/tests/intermediate
生成 oneOf
?
同样在 Child1Class
和 Child2Class
的 allOf
部分中,应该引用 IntermediateClass
,而不是 BaseClass
! (我试着在 IntermediateClass
中添加了一个 属性 但没有任何改变。)
我终于找到了这个问题的解决方案:为 IntermediateClass 添加一个@Schema-annotation。
@Schema(
type = "object",
title = "IntermediateClass",
subTypes = {Child1Class.class, Child2Class.class}
)
@JsonPropertyOrder({"type", "foo"})
public abstract class IntermediateClass extends BaseClass {
}
我们有一个 class 层次结构,如下所示:
基类:
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();
}
中间类扩展基类:
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;
@JsonPropertyOrder({"type", "foo"})
@Getter
@Setter
public abstract class IntermediateClass extends BaseClass {
}
Child1Class 扩展 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 也扩展了 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 Child2Class extends IntermediateClass {
private Integer foo;
}
直接扩展 BaseClass 的 Child3Class:
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;
}
Spring MVC 控制器(Spring Boot v2.5.2 应用程序)如下所示:
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();
}
}
虽然 Jackson 一切正常,SpringDoc 生成了以下 OpenAPI 规范:
{
"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"
}
}
}
}
}
}
因此,对于 /api/sscce/tests/base
,响应将按预期生成为 oneOf
。
我们如何让它也为 /api/sscce/tests/intermediate
生成 oneOf
?
同样在 Child1Class
和 Child2Class
的 allOf
部分中,应该引用 IntermediateClass
,而不是 BaseClass
! (我试着在 IntermediateClass
中添加了一个 属性 但没有任何改变。)
我终于找到了这个问题的解决方案:为 IntermediateClass 添加一个@Schema-annotation。
@Schema(
type = "object",
title = "IntermediateClass",
subTypes = {Child1Class.class, Child2Class.class}
)
@JsonPropertyOrder({"type", "foo"})
public abstract class IntermediateClass extends BaseClass {
}