|
5 | 5 | import io.swagger.v3.core.converter.ModelConverterContextImpl; |
6 | 6 | import io.swagger.v3.core.jackson.ModelResolver; |
7 | 7 | import io.swagger.v3.core.util.Json; |
| 8 | +import io.swagger.v3.core.util.Json31; |
8 | 9 | import io.swagger.v3.oas.models.media.ComposedSchema; |
9 | 10 | import io.swagger.v3.oas.models.media.Schema; |
10 | 11 | import org.testng.annotations.Test; |
11 | 12 |
|
| 13 | +import java.util.Collections; |
| 14 | + |
12 | 15 | import static org.testng.Assert.assertEquals; |
13 | 16 | import static org.testng.Assert.assertNotNull; |
| 17 | +import static org.testng.Assert.assertNull; |
14 | 18 | import static org.testng.Assert.assertTrue; |
15 | 19 |
|
16 | 20 | /** |
|
19 | 23 | * <p> |
20 | 24 | * {@code ModelResolver.resolveWrapping(...)} used to nest the whole model inside a new wrapper |
21 | 25 | * schema, which dropped the {@code oneOf} (the wrapper only copied name + properties). The |
22 | | - * composition is now preserved and the wrapper property is added alongside it. |
| 26 | + * composition is now preserved: a model with a {@code oneOf} is returned unchanged. |
| 27 | + * Per-subtype wrapping is not modelled yet. |
23 | 28 | */ |
24 | 29 | public class WrapperObjectOneOfTest { |
25 | 30 |
|
26 | 31 | @Test |
27 | 32 | public void wrapperObjectPreservesOneOf() { |
| 33 | + final Schema<?> model = resolve(new ModelResolver(Json.mapper()), WrappedOneOf.class); |
| 34 | + |
| 35 | + assertTrue(model instanceof ComposedSchema, |
| 36 | + "the explicit @Schema(oneOf) must produce a ComposedSchema, got: " + model.getClass().getName()); |
| 37 | + assertOneOfKept(model); |
| 38 | + } |
| 39 | + |
| 40 | + @Test |
| 41 | + public void wrapperObjectDoesNotAddWrapperProperty() { |
| 42 | + final Schema<?> model = resolve(new ModelResolver(Json.mapper()), WrappedOneOf.class); |
| 43 | + |
| 44 | + // only the declared member is present: no property named after the base type is added next to oneOf |
| 45 | + assertEquals(model.getProperties().keySet(), Collections.singleton("petType")); |
| 46 | + } |
| 47 | + |
| 48 | + @Test |
| 49 | + public void wrapperObjectOneOfIsStableWhenResolvedTwice() { |
28 | 50 | final ModelResolver modelResolver = new ModelResolver(Json.mapper()); |
29 | 51 | final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); |
30 | 52 |
|
31 | | - final Schema<?> model = context.resolve(new AnnotatedType(WrappedOneOf.class)); |
| 53 | + final Schema<?> first = context.resolve(new AnnotatedType(WrappedOneOf.class)); |
| 54 | + final Schema<?> second = context.resolve(new AnnotatedType(WrappedOneOf.class)); |
32 | 55 |
|
33 | | - assertTrue(model instanceof ComposedSchema, |
34 | | - "the explicit @Schema(oneOf) must produce a ComposedSchema, got: " + model.getClass().getName()); |
35 | | - final ComposedSchema composed = (ComposedSchema) model; |
36 | | - assertNotNull(composed.getOneOf(), "oneOf must be preserved when WRAPPER_OBJECT wrapping is applied"); |
37 | | - assertEquals(composed.getOneOf().size(), 2, "oneOf should reference both subtypes"); |
38 | | - assertEquals(composed.getOneOf().get(0).get$ref(), "#/components/schemas/Dog"); |
39 | | - assertEquals(composed.getOneOf().get(1).get$ref(), "#/components/schemas/Cat"); |
40 | | - // the WRAPPER_OBJECT property is added next to oneOf, not nested around it |
41 | | - assertNotNull(composed.getProperties(), "the wrapper property must be present alongside oneOf"); |
| 56 | + assertOneOfKept(first); |
| 57 | + assertOneOfKept(second); |
| 58 | + assertEquals(second.getProperties().keySet(), first.getProperties().keySet()); |
42 | 59 | } |
43 | 60 |
|
44 | | - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT, property = "pet") |
| 61 | + @Test |
| 62 | + public void wrapperObjectPreservesOneOfOpenApi31() { |
| 63 | + final Schema<?> model = resolve(new ModelResolver(Json31.mapper()).openapi31(true), WrappedOneOf.class); |
| 64 | + |
| 65 | + assertOneOfKept(model); |
| 66 | + assertEquals(model.getProperties().keySet(), Collections.singleton("petType")); |
| 67 | + } |
| 68 | + |
| 69 | + @Test |
| 70 | + public void wrapperObjectWithoutOneOfIsStillWrapped() { |
| 71 | + final Schema<?> model = resolve(new ModelResolver(Json.mapper()), PlainWrapped.class); |
| 72 | + |
| 73 | + assertNull(model.getOneOf(), "a model without oneOf must not gain one"); |
| 74 | + assertEquals(model.getProperties().keySet(), Collections.singleton("PlainWrapped")); |
| 75 | + final Schema<?> inner = (Schema<?>) model.getProperties().get("PlainWrapped"); |
| 76 | + assertEquals(inner.getProperties().keySet(), Collections.singleton("value")); |
| 77 | + } |
| 78 | + |
| 79 | + private static Schema<?> resolve(ModelResolver modelResolver, Class<?> cls) { |
| 80 | + return new ModelConverterContextImpl(modelResolver).resolve(new AnnotatedType(cls)); |
| 81 | + } |
| 82 | + |
| 83 | + private static void assertOneOfKept(Schema<?> model) { |
| 84 | + assertNotNull(model.getOneOf(), "oneOf must be preserved when WRAPPER_OBJECT wrapping is applied"); |
| 85 | + assertEquals(model.getOneOf().size(), 2, "oneOf should reference both subtypes"); |
| 86 | + assertEquals(model.getOneOf().get(0).get$ref(), "#/components/schemas/Dog"); |
| 87 | + assertEquals(model.getOneOf().get(1).get$ref(), "#/components/schemas/Cat"); |
| 88 | + } |
| 89 | + |
| 90 | + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) |
45 | 91 | @io.swagger.v3.oas.annotations.media.Schema(oneOf = {Dog.class, Cat.class}) |
46 | 92 | static abstract class WrappedOneOf { |
47 | 93 | public String petType; |
48 | 94 | } |
49 | 95 |
|
| 96 | + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) |
| 97 | + static class PlainWrapped { |
| 98 | + public String value; |
| 99 | + } |
| 100 | + |
50 | 101 | static class Dog extends WrappedOneOf { |
51 | 102 | public String bark; |
52 | 103 | } |
|
0 commit comments