Skip to content
Open
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 @@ -2682,6 +2682,11 @@ protected void resolveDiscriminatorProperty(JavaType type, ModelConverterContext
}
}

/** An empty object schema for the current OpenAPI version. */
private Schema newObjectSchema() {
return openapi31 ? new JsonSchema().typesItem("object") : new ObjectSchema();
}

/*
TODO partial implementation supporting WRAPPER_OBJECT with JsonTypeInfo.Id.CLASS and JsonTypeInfo.Id.NAME

Expand All @@ -2706,7 +2711,14 @@ protected Schema resolveWrapping(JavaType type, ModelConverterContext context, S
if (JsonTypeInfo.Id.NAME.equals(id) && name == null) {
name = type.getRawClass().getSimpleName();
}
Schema wrapperSchema = openapi31 ? new JsonSchema().typesItem("object") : new ObjectSchema();
// If the model already carries a oneOf (e.g. from @Schema(oneOf) or @JsonSubTypes),
// adding the wrapper property alongside it preserves the composition instead of
// nesting the whole model (and dropping the oneOf) inside a wrapper schema.
if (model.getOneOf() != null && !model.getOneOf().isEmpty()) {
model.addProperties(name, newObjectSchema());
return model;
}
Schema wrapperSchema = newObjectSchema();
wrapperSchema.name(model.getName());
wrapperSchema.addProperties(name, model);
return wrapperSchema;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.swagger.v3.core.resolving;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverterContextImpl;
import io.swagger.v3.core.jackson.ModelResolver;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

/**
* A schema that already carries a {@code oneOf} must keep it when
* {@code @JsonTypeInfo(include = WRAPPER_OBJECT)} wrapping is applied.
* <p>
* {@code ModelResolver.resolveWrapping(...)} used to nest the whole model inside a new wrapper
* schema, which dropped the {@code oneOf} (the wrapper only copied name + properties). The
* composition is now preserved and the wrapper property is added alongside it.
*/
public class WrapperObjectOneOfTest {

@Test
public void wrapperObjectPreservesOneOf() {
final ModelResolver modelResolver = new ModelResolver(Json.mapper());
final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver);

final Schema<?> model = context.resolve(new AnnotatedType(WrappedOneOf.class));

assertTrue(model instanceof ComposedSchema,
"the explicit @Schema(oneOf) must produce a ComposedSchema, got: " + model.getClass().getName());
final ComposedSchema composed = (ComposedSchema) model;
assertNotNull(composed.getOneOf(), "oneOf must be preserved when WRAPPER_OBJECT wrapping is applied");
assertEquals(composed.getOneOf().size(), 2, "oneOf should reference both subtypes");
assertEquals(composed.getOneOf().get(0).get$ref(), "#/components/schemas/Dog");
assertEquals(composed.getOneOf().get(1).get$ref(), "#/components/schemas/Cat");
// the WRAPPER_OBJECT property is added next to oneOf, not nested around it
assertNotNull(composed.getProperties(), "the wrapper property must be present alongside oneOf");
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT, property = "pet")
@io.swagger.v3.oas.annotations.media.Schema(oneOf = {Dog.class, Cat.class})
static abstract class WrappedOneOf {
public String petType;
}

static class Dog extends WrappedOneOf {
public String bark;
}

static class Cat extends WrappedOneOf {
public String meow;
}
}