-
-
Notifications
You must be signed in to change notification settings - Fork 553
Description
Consider the below API:
public class MyApi {
@GET
@Path("/random")
@Produces(MediaType.APPLICATION_JSON)
public Pet random() {
Random rand = new Random();
if (rand.nextBoolean())
return new Cat(rand.nextBoolean());
else
return new Dog(rand.nextBoolean());
}
}
@Schema(
type = "object",
title = "Pet",
subTypes = { Cat.class, Dog.class },
discriminatorMapping = {
@DiscriminatorMapping( value = "CAT", schema = Cat.class ),
@DiscriminatorMapping( value = "DOG", schema = Dog.class )
},
discriminatorProperty = "petTypeAsString"
)
public class Pet {
public enum PetType {
CAT,
DOG;
}
@Schema(required = true)
public PetType petType;
@Schema(required = true)
public String petTypeAsString;
public Pet() {}
public Pet(PetType petType) {
this.petType = petType;
this.petTypeAsString = petType.name();
}
}
public class Cat extends Pet{
public boolean hunts;
public Cat() {
super(Pet.PetType.CAT);
}
public Cat(boolean hunts) {
this();
this.hunts = hunts;
}
}
public class Dog extends Pet{
public boolean barks;
public Dog() {
super(Pet.PetType.DOG);
}
public Dog(boolean barks) {
this();
this.barks = barks;
}
}
The above example properly generates Cat and Dog with allOf: - $ref: '#/components/schemas/Pet', so I can use a client side generator to correctly reproduce the type heirarchy.
If I add a new method like this:
@PUT
@Path("/putDog")
@Produces(MediaType.APPLICATION_JSON)
public void putDog(Dog d) {}
Now, my spec generates with Dog missing the allOf:
Dog:
"type": "object"
"properties": {
"barks": {
"type" : boolean"
},
"petType": {
"type": "string",
enum: [ "CAT", "DOG" ]
},
"petTypeAsString": {
"type": "string"
}
},
"required": ["petType", "petTypeAsString"]
This has the correct properties, but since the allOf is lost, I can no longer generate this as a subclass of Pet correctly.
It turns out in fact you can force the allOf to generate again by declaring a method with the abstract type as the parameter e.g.
@PUT
@Path("/putPet")
@Produces(MediaType.APPLICATION_JSON)
public void putPet(Pet p) {
if (p instanceOf Dog) putDog((Dog) p)
else ....
}
Given that the /random method can already return Pet, this seems to be a bug. I shouldn't need to provide putPet just to force it to respect the polymorphism of Pet if I want two separate putCat and putDog methods. Stepping through in the debugger, I can see that when we process /random there is a ComposedSchema generated, but this is later replaced when we process putDog - the code should be reusing the generated Schema for Dog from /random, not creating an incorrect one that treats it as a concrete class with no super types.
This also bites if you have a class like
public class ManyPets {
public final Array[Pet] thePets;
public ManyPets(Array[Pet] pets) {
this.thePets = pets;
}
}
and methods like
@PUT
@Path("/putCat")
@Produces(MediaType.APPLICATION_JSON)
public void putDog(Cat c) {}
@PUT
@Path("/putDog")
@Produces(MediaType.APPLICATION_JSON)
public void putDog(Dog d) {}
@PUT
@Path("/putMultiple")
@Produces(MediaType.APPLICATION_JSON)
public void putMultiple(ManyPets pets) {}
The putMultiple path will correctly generate ComposedSchemas for Cat and Dog with allOfs present, but the two other puts will replace those schemas and strip the allOf from the final spec.
Code generated via an openApi client generator will now be unable to see that Cat and Dog are suitable subtypes to provide to ManyPets.
This happens with or without adding the property "springdoc.model-converters.polymorphic-converter.enabled=false" which I have seen on some other related issues.
I'm reasonably sure this worked on springdoc-openapi 1.4.0 and could be convinced to generate correctly with no workarounds, and broke in 1.6.6. That said, I can't retry with 1.4.0 as it had log4j vulnerabilities