Skip to content

Use oneOf schema for polymorphic types #654

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

Merged
merged 1 commit into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Compon
JsonView jsonView, Annotation[] annotations) {
Schema schemaObject = extractSchema(components, schemaImplementation, jsonView, annotations);
if (schemaObject != null && StringUtils.isBlank(schemaObject.get$ref())
&& StringUtils.isBlank(schemaObject.getType())) {
&& StringUtils.isBlank(schemaObject.getType()) && !(schemaObject instanceof ComposedSchema)) {
// default to string
schemaObject.setType("string");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springdoc.core.converters.AdditionalModelsConverter;
import org.springdoc.core.converters.FileSupportConverter;
import org.springdoc.core.converters.ModelConverterRegistrar;
import org.springdoc.core.converters.PolymorphicModelConverter;
import org.springdoc.core.converters.PropertyCustomizingConverter;
import org.springdoc.core.converters.ResponseSupportConverter;
import org.springdoc.core.converters.SchemaPropertyDeprecatingConverter;
Expand Down Expand Up @@ -84,7 +85,7 @@ LocalVariableTableParameterNameDiscoverer localSpringDocParameterNameDiscoverer(

@Bean
@Lazy(false)
AdditionalModelsConverter pageableSupportConverter() {
AdditionalModelsConverter additionalModelsConverter() {
return new AdditionalModelsConverter();
}

Expand Down Expand Up @@ -115,6 +116,13 @@ SchemaPropertyDeprecatingConverter schemaPropertyDeprecatingConverter() {
return new SchemaPropertyDeprecatingConverter();
}

@Bean
@ConditionalOnMissingBean
@Lazy(false)
PolymorphicModelConverter polymorphicModelConverter() {
return new PolymorphicModelConverter();
}

@Bean
@ConditionalOnMissingBean
OpenAPIBuilder openAPIBuilder(Optional<OpenAPI> openAPI, ApplicationContext context,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
*
* *
* * * Copyright 2019-2020 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 org.springdoc.core.converters;

import java.lang.reflect.Modifier;
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;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema;

public class PolymorphicModelConverter implements ModelConverter {
@Override
public Schema resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) {
if (chain.hasNext()) {
Schema<?> resolvedSchema = chain.next().resolve(type, context, chain);
if (resolvedSchema == null || resolvedSchema.get$ref() == null) return resolvedSchema;
return composePolymorphicSchema(type, resolvedSchema, context.getDefinedModels().values());
}
return null;
}

private Schema composePolymorphicSchema(AnnotatedType type, Schema schema, Collection<Schema> schemas) {
String ref = schema.get$ref();
List<Schema> composedSchemas = schemas.stream()
.filter(s -> s instanceof ComposedSchema)
.map(s -> (ComposedSchema) s)
.filter(s -> s.getAllOf() != null)
.filter(s -> s.getAllOf().stream().anyMatch(s2 -> ref.equals(s2.get$ref())))
.map(s -> new Schema().$ref("#/components/schemas/" + s.getName()))
.collect(Collectors.toList());
if (composedSchemas.isEmpty()) return schema;

ComposedSchema result = new ComposedSchema();
if (isConcreteClass(type)) result.addOneOfItem(schema);
composedSchemas.forEach(result::addOneOfItem);
return result;
}

private boolean isConcreteClass(AnnotatedType type) {
JavaType javaType = Json.mapper().constructType(type.getType());
Class<?> clazz = javaType.getRawClass();
return !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isInterface();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package test.org.springdoc.api.app118;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

@JsonTypeInfo(use = Id.NAME, property = "type")
@JsonSubTypes({
@Type(ChildOfAbstract1.class),
@Type(ChildOfAbstract2.class)
})
public abstract class AbstractParent {
private int id;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}
}

class ChildOfAbstract1 extends AbstractParent {
private String abstrachChild1Param;

public String getAbstrachChild1Param() {
return abstrachChild1Param;
}

public void setAbstrachChild1Param(String abstrachChild1Param) {
this.abstrachChild1Param = abstrachChild1Param;
}
}

class ChildOfAbstract2 extends AbstractParent {
private String abstractChild2Param;

public String getAbstractChild2Param() {
return abstractChild2Param;
}

public void setAbstractChild2Param(String abstractChild2Param) {
this.abstractChild2Param = abstractChild2Param;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package test.org.springdoc.api.app118;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

@JsonTypeInfo(use = Id.NAME, property = "type")
@JsonSubTypes({
@Type(ChildOfConcrete1.class),
@Type(ChildOfConcrete2.class)
})
public class ConcreteParent {
private int id;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}
}

class ChildOfConcrete1 extends ConcreteParent {
private String concreteChild1Param;

public String getConcreteChild1Param() {
return concreteChild1Param;
}

public void setConcreteChild1Param(String concreteChild1Param) {
this.concreteChild1Param = concreteChild1Param;
}
}

class ChildOfConcrete2 extends ConcreteParent {
private String concreteChild2Param;

public String getConcreteChild2Param() {
return concreteChild2Param;
}

public void setConcreteChild2Param(String concreteChild2Param) {
this.concreteChild2Param = concreteChild2Param;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package test.org.springdoc.api.app118;

import java.util.List;

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

@RestController
@RequestMapping("class-hierarchy")
public class Controller {
@PostMapping("abstract-parent")
public Response abstractParent(@RequestBody AbstractParent payload) {
return null;
}

@PostMapping("concrete-parent")
public Response concreteParent(@RequestBody ConcreteParent payload) {
return null;
}
}

class Response {
AbstractParent abstractParent;

List<ConcreteParent> concreteParents;

public AbstractParent getAbstractParent() {
return abstractParent;
}

public void setAbstractParent(AbstractParent abstractParent) {
this.abstractParent = abstractParent;
}

public List<ConcreteParent> getConcreteParents() {
return concreteParents;
}

public void setConcreteParents(List<ConcreteParent> concreteParents) {
this.concreteParents = concreteParents;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package test.org.springdoc.api.app118;

import test.org.springdoc.api.AbstractSpringDocTest;

import org.springframework.boot.autoconfigure.SpringBootApplication;


public class SpringDocApp118Test extends AbstractSpringDocTest {

@SpringBootApplication
static class SpringDocTestApp {}
}
Loading