99import io .swagger .v3 .oas .annotations .media .DiscriminatorMapping ;
1010import io .swagger .v3 .oas .models .media .ComposedSchema ;
1111import io .swagger .v3 .oas .models .media .Schema ;
12+ import org .testng .annotations .AfterMethod ;
1213import org .testng .annotations .AfterTest ;
1314import org .testng .annotations .BeforeMethod ;
1415import org .testng .annotations .Test ;
1516
16- import java .util .HashSet ;
17- import java .util .List ;
1817import java .util .Map ;
19- import java .util .Set ;
2018
2119import static org .testng .Assert .*;
2220
@@ -37,13 +35,20 @@ public class JsonSubTypesAndSchemaOneOfTest extends SwaggerTestBase {
3735 @ BeforeMethod
3836 public void setup () {
3937 ModelResolver .composedModelPropertiesAsSibling = false ;
38+ ModelResolver .jsonSubTypesOneOf = false ;
4039 ModelResolver modelResolver = new ModelResolver (new ObjectMapper ());
4140 context = new ModelConverterContextImpl (modelResolver );
4241 }
4342
43+ @ AfterMethod
44+ public void resetJsonSubTypesOneOf () {
45+ ModelResolver .jsonSubTypesOneOf = false ;
46+ }
47+
4448 @ AfterTest
4549 public void afterTest () {
4650 ModelResolver .composedModelPropertiesAsSibling = false ;
51+ ModelResolver .jsonSubTypesOneOf = false ;
4752 }
4853
4954 @ Test
@@ -195,46 +200,180 @@ static class SubBean2InterfaceImplementor extends BaseBeanInterfaceImplementor {
195200 }
196201
197202 @ Test
198- public void duplicateJsonSubTypesEntriesAreDeduplicated () {
199- // Identical @JsonSubTypes entries (and Jackson's AnnotationIntrospectorPair) can report the
200- // same subtype more than once. When the parent is composed into a oneOf, each entry would
201- // otherwise become a repeated $ref.
202- //
203- // NOTE: on 3.0.0 alone the parent is not composed into a oneOf, so this test passes
204- // trivially. It only starts guarding against duplicates once the @JsonSubTypes -> oneOf
205- // composition lands (swagger-api/swagger-core#5320); with that change and without the
206- // dedup fix in resolveSubtypes(), the oneOf contains 3 entries instead of 2.
207- final Schema <?> model = context .resolve (new AnnotatedType (DuplicateSubTypesParent .class ));
208-
209- if (model instanceof ComposedSchema && ((ComposedSchema ) model ).getOneOf () != null ) {
210- final List <Schema > oneOf = ((ComposedSchema ) model ).getOneOf ();
211- assertEquals (oneOf .size (), 2 , "duplicate @JsonSubTypes entries must be deduplicated" );
212- final Set <String > refs = new HashSet <>();
213- for (Schema <?> item : oneOf ) {
214- assertTrue (refs .add (item .get$ref ()),
215- "oneOf must not contain a repeated $ref: " + item .get$ref ());
216- }
203+ public void jsonSubTypesOnlyParentHasNoOneOfByDefault () {
204+ // Default behavior (opt-in disabled): a @JsonSubTypes-only type is NOT composed into a oneOf,
205+ // matching the 3.0.0 behavior.
206+ ModelResolver .jsonSubTypesOneOf = false ;
207+ final Schema <?> model = context .resolve (new AnnotatedType (PolymorphicInterface .class ));
208+ assertNotNull (model );
209+ assertTrue (model .getOneOf () == null || model .getOneOf ().isEmpty (),
210+ "with the default (opt-in disabled) the @JsonSubTypes-only parent must not be composed into a oneOf" );
211+ }
212+
213+ @ Test
214+ public void jsonSubTypesOnlyParentIsComposedIntoOneOfWhenOptedIn () {
215+ ModelResolver .jsonSubTypesOneOf = true ;
216+ final Schema <?> model = context .resolve (new AnnotatedType (PolymorphicInterface .class ));
217+ assertTrue (model instanceof ComposedSchema ,
218+ "when opted in the @JsonSubTypes-only parent should be converted to a oneOf ComposedSchema" );
219+ assertNotNull (model .getOneOf ());
220+ assertEquals (model .getOneOf ().size (), 2 );
221+ assertEquals (model .getOneOf ().get (0 ).get$ref (), "#/components/schemas/PolymorphicSubtype1" );
222+ assertEquals (model .getOneOf ().get (1 ).get$ref (), "#/components/schemas/PolymorphicSubtype2" );
223+ }
224+
225+ @ JsonTypeInfo (use = JsonTypeInfo .Id .NAME , include = JsonTypeInfo .As .PROPERTY , property = "type" , visible = true )
226+ @ JsonSubTypes ({
227+ @ JsonSubTypes .Type (value = PolymorphicSubtype1 .class , name = "subtype1" ),
228+ @ JsonSubTypes .Type (value = PolymorphicSubtype2 .class , name = "subtype2" )
229+ })
230+ interface PolymorphicInterface {
231+ String getType ();
232+ String getName ();
233+ }
234+
235+ static class PolymorphicSubtype1 implements PolymorphicInterface {
236+ public String type = "subtype1" ;
237+ public String name = "subtype1" ;
238+
239+ @ Override
240+ public String getType () {
241+ return type ;
217242 }
218243
219- // Regardless of composition, each distinct subtype schema must be defined.
220- assertTrue (context .getDefinedModels ().containsKey ("DuplicateSubTypesChild1" ));
221- assertTrue (context .getDefinedModels ().containsKey ("DuplicateSubTypesChild2" ));
244+ @ Override
245+ public String getName () {
246+ return name ;
247+ }
248+ }
249+
250+ static class PolymorphicSubtype2 implements PolymorphicInterface {
251+ public String type = "subtype2" ;
252+ public String name = "subtype2" ;
253+
254+ @ Override
255+ public String getType () {
256+ return type ;
257+ }
258+
259+ @ Override
260+ public String getName () {
261+ return name ;
262+ }
263+ }
264+
265+ @ Test
266+ public void optInKeepsSubtypesAllOfParentAndParentProperties () {
267+ ModelResolver .jsonSubTypesOneOf = true ;
268+ final Schema <?> parent = context .resolve (new AnnotatedType (PolymorphicInterface .class ));
269+ assertTrue (parent instanceof ComposedSchema );
270+ assertNotNull (parent .getProperties (), "parent properties must survive the replacement" );
271+ assertTrue (parent .getProperties ().containsKey ("name" ));
272+
273+ final Schema <?> sub = context .getDefinedModels ().get ("PolymorphicSubtype1" );
274+ assertTrue (sub instanceof ComposedSchema );
275+ assertEquals (((ComposedSchema ) sub ).getAllOf ().get (0 ).get$ref (), "#/components/schemas/PolymorphicInterface" );
222276 }
223277
224- @ JsonTypeInfo (use = JsonTypeInfo .Id .NAME , property = "type" )
278+ @ Test
279+ public void optInOneOfFollowsDeclarationOrderAndUsesResolvedNames () {
280+ ModelResolver .jsonSubTypesOneOf = true ;
281+ final Schema <?> parent = context .resolve (new AnnotatedType (RenamedParent .class ));
282+ assertNotNull (parent .getOneOf ());
283+ assertEquals (parent .getOneOf ().size (), 2 );
284+ assertEquals (parent .getOneOf ().get (0 ).get$ref (), "#/components/schemas/RenamedFirst" );
285+ assertEquals (parent .getOneOf ().get (1 ).get$ref (), "#/components/schemas/RenamedSecond" );
286+ for (Schema <?> ref : parent .getOneOf ()) {
287+ final String name = ref .get$ref ().substring ("#/components/schemas/" .length ());
288+ assertNotNull (context .getDefinedModels ().get (name ), "dangling $ref in oneOf: " + name );
289+ }
290+ }
291+
292+ @ Test
293+ public void optInExplicitSchemaOneOfWinsOverJsonSubTypes () {
294+ ModelResolver .jsonSubTypesOneOf = true ;
295+ final Schema <?> model = context .resolve (new AnnotatedType (BaseBean .class ));
296+ assertNotNull (model .getOneOf ());
297+ assertEquals (model .getOneOf ().size (), 2 );
298+ assertEquals (model .getOneOf ().get (0 ).get$ref (), "#/components/schemas/SubBean1" );
299+ assertEquals (model .getOneOf ().get (1 ).get$ref (), "#/components/schemas/SubBean2" );
300+
301+ // subtype handling and discriminator are identical to the flag-off output
302+ assertEquals (model .getDiscriminator ().getPropertyName (), DISCRIMINATOR_PROPERTY_NAME );
303+ assertEquals (model .getDiscriminator ().getMapping ().get (SUB_BEAN_1_NAME ), "#/components/schemas/SubBean1" );
304+ assertEquals (model .getDiscriminator ().getMapping ().get (SUB_BEAN_2_NAME ), "#/components/schemas/SubBean2" );
305+ for (String subName : new String []{SUB_BEAN_1_NAME , SUB_BEAN_2_NAME }) {
306+ final Schema <?> sub = context .getDefinedModels ().get (subName );
307+ assertTrue (sub instanceof ComposedSchema , subName + " must be composed" );
308+ assertEquals (((ComposedSchema ) sub ).getAllOf ().get (0 ).get$ref (), "#/components/schemas/BaseBean" );
309+ }
310+ }
311+
312+ @ Test
313+ public void optInProducesOneOfRefsForOpenApi31 () {
314+ ModelResolver .jsonSubTypesOneOf = true ;
315+ final ModelConverterContextImpl ctx31 =
316+ new ModelConverterContextImpl (new ModelResolver (new ObjectMapper ()).openapi31 (true ));
317+ final Schema <?> model = ctx31 .resolve (new AnnotatedType (PolymorphicInterface .class ));
318+ assertNotNull (model .getOneOf ());
319+ assertEquals (model .getOneOf ().size (), 2 );
320+ assertEquals (model .getOneOf ().get (0 ).get$ref (), "#/components/schemas/PolymorphicSubtype1" );
321+ }
322+
323+ @ Test
324+ public void defaultOutputUnchangedForSubtypesParent () {
325+ ModelResolver .jsonSubTypesOneOf = false ;
326+ final Schema <?> parent = context .resolve (new AnnotatedType (PolymorphicInterface .class ));
327+ assertFalse (parent instanceof ComposedSchema && ((ComposedSchema ) parent ).getOneOf () != null );
328+ assertTrue (parent .getProperties ().containsKey ("name" ));
329+ final Schema <?> sub = context .getDefinedModels ().get ("PolymorphicSubtype1" );
330+ assertTrue (sub instanceof ComposedSchema );
331+ assertEquals (((ComposedSchema ) sub ).getAllOf ().get (0 ).get$ref (), "#/components/schemas/PolymorphicInterface" );
332+ }
333+
334+ @ Test
335+ public void optInOneOfSkipsSubtypesNotAssignableToParent () {
336+ ModelResolver .jsonSubTypesOneOf = true ;
337+ final Schema <?> parent = context .resolve (new AnnotatedType (ParentWithUnrelatedSubtype .class ));
338+ assertNotNull (parent .getOneOf ());
339+ assertEquals (parent .getOneOf ().size (), 1 );
340+ assertEquals (parent .getOneOf ().get (0 ).get$ref (), "#/components/schemas/RelatedImpl" );
341+ }
342+
343+ @ JsonTypeInfo (use = JsonTypeInfo .Id .NAME , include = JsonTypeInfo .As .PROPERTY , property = "kind" )
225344 @ JsonSubTypes ({
226- @ JsonSubTypes .Type (value = DuplicateSubTypesChild1 .class ),
227- @ JsonSubTypes .Type (value = DuplicateSubTypesChild1 .class ),
228- @ JsonSubTypes .Type (value = DuplicateSubTypesChild2 .class )
345+ @ JsonSubTypes .Type (value = RelatedImpl .class , name = "related" ),
346+ @ JsonSubTypes .Type (value = UnrelatedType .class , name = "unrelated" )
229347 })
230- static abstract class DuplicateSubTypesParent {
231- public String type ;
348+ static class ParentWithUnrelatedSubtype {
349+ public String id ;
350+ }
351+
352+ static class RelatedImpl extends ParentWithUnrelatedSubtype {
353+ public String x ;
354+ }
355+
356+ static class UnrelatedType {
357+ public String y ;
358+ }
359+
360+ @ JsonTypeInfo (use = JsonTypeInfo .Id .NAME , include = JsonTypeInfo .As .PROPERTY , property = "kind" )
361+ @ JsonSubTypes ({
362+ @ JsonSubTypes .Type (value = RenamedFirstImpl .class , name = "first" ),
363+ @ JsonSubTypes .Type (value = RenamedSecondImpl .class , name = "second" )
364+ })
365+ static class RenamedParent {
366+ public String id ;
232367 }
233368
234- static class DuplicateSubTypesChild1 extends DuplicateSubTypesParent {
369+ @ io .swagger .v3 .oas .annotations .media .Schema (name = "RenamedFirst" )
370+ static class RenamedFirstImpl extends RenamedParent {
371+ public String x ;
235372 }
236373
237- static class DuplicateSubTypesChild2 extends DuplicateSubTypesParent {
374+ @ io .swagger .v3 .oas .annotations .media .Schema (name = "RenamedSecond" )
375+ static class RenamedSecondImpl extends RenamedParent {
376+ public String y ;
238377 }
239378
240379 private void assertSubPropertiesValid (Map <String , Schema > subProperties , final String childPropertyName ) {
0 commit comments