Skip to content

Commit

Permalink
PolymorphicModelConverter only handles direct subtypes and misses ind…
Browse files Browse the repository at this point in the history
…irect. Fixes #2603
  • Loading branch information
bnasslahsen committed Jun 16, 2024
1 parent 07e2c92 commit 497bfae
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
package org.springdoc.core.converters;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;

import com.fasterxml.jackson.databind.JavaType;
import io.swagger.v3.core.converter.AnnotatedType;
Expand Down Expand Up @@ -61,10 +63,12 @@ public PolymorphicModelConverter(ObjectMapperProvider springDocObjectMapper) {

private static Schema<?> getResolvedSchema(JavaType javaType, Schema<?> resolvedSchema) {
if (resolvedSchema instanceof ObjectSchema && resolvedSchema.getProperties() != null) {
if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getName()))
if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getName())){
resolvedSchema = resolvedSchema.getProperties().get(javaType.getRawClass().getName());
else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSimpleName()))
}
else if (resolvedSchema.getProperties().containsKey(javaType.getRawClass().getSimpleName())){
resolvedSchema = resolvedSchema.getProperties().get(javaType.getRawClass().getSimpleName());
}
}
return resolvedSchema;
}
Expand Down Expand Up @@ -94,13 +98,8 @@ public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterato
*/
private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Collection<Schema> schemas) {
String ref = schema.get$ref();
List<Schema> composedSchemas = schemas.stream()
.filter(ComposedSchema.class::isInstance)
.map(ComposedSchema.class::cast)
.filter(s -> s.getAllOf() != null)
.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
.map(s -> new Schema().$ref(AnnotationsUtils.COMPONENTS_REF + s.getName()))
.toList();
List<Schema> composedSchemas = findComposedSchemas(ref, schemas);

if (composedSchemas.isEmpty()) return schema;

ComposedSchema result = new ComposedSchema();
Expand All @@ -109,6 +108,31 @@ private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Colle
return result;
}

/**
* Find composed schemas recursively.
*
* @param ref the reference of the schema
* @param schemas the collection of schemas to search in
* @return the list of composed schemas
*/
private List<Schema> findComposedSchemas(String ref, Collection<Schema> schemas) {
List<Schema> composedSchemas = schemas.stream()
.filter(ComposedSchema.class::isInstance)
.map(ComposedSchema.class::cast)
.filter(s -> s.getAllOf() != null)
.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
.map(s -> new Schema().$ref(AnnotationsUtils.COMPONENTS_REF + s.getName()))
.collect(Collectors.toList());

List<Schema> resultSchemas = new ArrayList<>(composedSchemas);

for (Schema childSchema : composedSchemas) {
String childSchemaRef = childSchema.get$ref();
resultSchemas.addAll(findComposedSchemas(childSchemaRef, schemas));
}

return resultSchemas;
}
/**
* Is concrete class boolean.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package test.org.springdoc.api.v30.app220;


import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.oas.annotations.media.Schema;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

@PostMapping("/parent")
public void parentEndpoint(@RequestBody Superclass parent) {

}

}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
@JsonSubTypes({
@Type(value = IntermediateClass.class, name = IntermediateClass.SCHEMA_NAME),
})
sealed class Superclass permits IntermediateClass {

public Superclass() {}
}

@Schema(name = IntermediateClass.SCHEMA_NAME)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
@JsonSubTypes({
@Type(value = FirstChildClass.class, name = FirstChildClass.SCHEMA_NAME),
@Type(value = SecondChildClass.class, name = SecondChildClass.SCHEMA_NAME)
})
sealed class IntermediateClass extends Superclass permits FirstChildClass, SecondChildClass {

public static final String SCHEMA_NAME = "IntermediateClass";
}

@Schema(name = FirstChildClass.SCHEMA_NAME)
final class FirstChildClass extends IntermediateClass {

public static final String SCHEMA_NAME = "Image";
}

@Schema(name = SecondChildClass.SCHEMA_NAME)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type")
@JsonSubTypes({
@Type(value = ThirdChildClass.class, name = ThirdChildClass.SCHEMA_NAME)
})
sealed class SecondChildClass extends IntermediateClass {

public static final String SCHEMA_NAME = "Mail";
}

@Schema(name = ThirdChildClass.SCHEMA_NAME)
final class ThirdChildClass extends SecondChildClass {

public static final String SCHEMA_NAME = "Home";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
*
* *
* * *
* * * *
* * * * * Copyright 2019-2022 the original author or authors.
* * * * *
* * * * * Licensed under the Apache License, Version 2.0 (the "License");
* * * * * you may not use this file except in compliance with the License.
* * * * * You may obtain a copy of the License at
* * * * *
* * * * * https://www.apache.org/licenses/LICENSE-2.0
* * * * *
* * * * * Unless required by applicable law or agreed to in writing, software
* * * * * distributed under the License is distributed on an "AS IS" BASIS,
* * * * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * * * * See the License for the specific language governing permissions and
* * * * * limitations under the License.
* * * *
* * *
* *
*
*/

package test.org.springdoc.api.v30.app220;

import test.org.springdoc.api.v30.AbstractSpringDocV30Test;

import org.springframework.boot.autoconfigure.SpringBootApplication;

public class SpringDocApp220Test extends AbstractSpringDocV30Test {

@SpringBootApplication
static class SpringDocTestApp {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{
"openapi": "3.0.1",
"info": {
"title": "OpenAPI definition",
"version": "v0"
},
"servers": [
{
"url": "http://localhost",
"description": "Generated server url"
}
],
"paths": {
"/parent": {
"post": {
"tags": [
"hello-controller"
],
"operationId": "parentEndpoint",
"requestBody": {
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/Superclass"
},
{
"$ref": "#/components/schemas/IntermediateClass"
},
{
"$ref": "#/components/schemas/Image"
},
{
"$ref": "#/components/schemas/Mail"
},
{
"$ref": "#/components/schemas/Home"
}
]
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
"schemas": {
"Home": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/Mail"
}
]
},
"Image": {
"type": "object",
"allOf": [
{
"$ref": "#/components/schemas/IntermediateClass"
}
]
},
"IntermediateClass": {
"required": [
"@type"
],
"type": "object",
"discriminator": {
"propertyName": "@type"
},
"allOf": [
{
"$ref": "#/components/schemas/Superclass"
},
{
"type": "object",
"properties": {
"@type": {
"type": "string"
}
}
}
]
},
"Mail": {
"required": [
"@type"
],
"type": "object",
"discriminator": {
"propertyName": "@type"
},
"allOf": [
{
"$ref": "#/components/schemas/IntermediateClass"
},
{
"type": "object",
"properties": {
"@type": {
"type": "string"
}
}
}
]
},
"Superclass": {
"required": [
"@type"
],
"type": "object",
"properties": {
"@type": {
"type": "string"
}
},
"discriminator": {
"propertyName": "@type"
}
}
}
}
}

0 comments on commit 497bfae

Please sign in to comment.