Skip to content

Polymorphism is incorrectly lost depending on available paths #1581

@slw546

Description

@slw546

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions