Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composition/Inheritance/Polymorphism issues (v2.0.6) #3046

Closed
catbref opened this issue Dec 6, 2018 · 5 comments
Closed

Composition/Inheritance/Polymorphism issues (v2.0.6) #3046

catbref opened this issue Dec 6, 2018 · 5 comments

Comments

@catbref
Copy link

catbref commented Dec 6, 2018

I'm trying to build an API where some responses can be one of many sub-classes.

In this simple toy API, the "GET /pets/random" is to return one Pet which could be either a Cat or a Dog.
So the Java function PetResource.random() returns Pet.
The "discrimination" property is a member of the parent Pet class.

However, the JSON output only ever reports properties from the parent Pet class:

{
  "petType": "CAT",
  "petTypeAsString": "CAT"
}

when I'm expecting

{
  "petType": "CAT",
  "petTypeAsString": "CAT",
  "hunts": true
}

I've tried various ways of annotating both PetResource, Pet and the Cat/Dog sub-classes to no avail.

In addition to this, there's a simpler "GET /pets/cat" which returns (one) Cat but also an extraneous field "type" which I can't seem to locate/remove:

{
  "type": "cat",
  "petType": "CAT",
  "petTypeAsString": "CAT",
  "hunts": true
}

Two (related) bugs here? Or what am I doing wrong?

Can't seem to attach Java files so...

PetResource.java:

package api;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import models.Cat;
import models.Dog;
import models.Pet;

@Path("/pets")
@Produces({MediaType.APPLICATION_JSON})
public class PetResource {

	@GET
	@Path("/cat")
	@Operation(
		summary = "Generate a cat",
		responses = {
			@ApiResponse(
				description = "a cat",
				content = @Content(
					mediaType = MediaType.APPLICATION_JSON,
					schema = @Schema(
						implementation = Cat.class
					)
				)
			)
		}
	)
	public Cat cat() {
		Random rand = new Random();

		return new Cat(rand.nextBoolean());
	}

	@GET
	@Path("/random")
	@Operation(
		summary = "Generate a pet",
		responses = {
			@ApiResponse(
				description = "a pet",
				content = @Content(
					mediaType = MediaType.APPLICATION_JSON,
					schema = @Schema(
						type = "object",
						title = "Pet",
						oneOf = { Cat.class, Dog.class },
						discriminatorMapping = {
								@DiscriminatorMapping( value = "CAT", schema = Cat.class ),
								@DiscriminatorMapping( value = "DOG", schema = Dog.class )
						},
						discriminatorProperty = "petTypeAsString"
					)
				)
			)
		}
	)
	public Pet random() {
		Random rand = new Random();

		if (rand.nextBoolean())
			return new Cat(rand.nextBoolean());
		else
			return new Dog(rand.nextBoolean());
	}

}

Pet.java:

package models;

import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(
		type = "object",
		title = "Pet",
		oneOf = { 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();
	}

}

Cat.java:

package models;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(
	name = "CAT",
	title = "Cat",
	allOf = Pet.class
)
public class Cat extends Pet {

	public boolean hunts;

	public Cat() {
		super(Pet.PetType.CAT);
	}

	public Cat(boolean hunts) {
		this();
		this.hunts = hunts;
	}

}

Dog.java:

package models;

import io.swagger.v3.oas.annotations.media.Schema;

@Schema(
	name = "DOG",
	title = "Dog",
	allOf = Pet.class
)
public class Dog extends Pet {

	public boolean barks;

	public Dog() {
		super(Pet.PetType.DOG);
	}

	public Dog(boolean barks) {
		this();
		this.barks = barks;
	}

}
@catbref
Copy link
Author

catbref commented Dec 14, 2018

I have a work-around for this using @XmlSeeAlso annotation in the parent "Pet" class.
Unfortunately I have to list all known sub-classes in the @XmlSeeAlso annotation which doesn't 'smell' right.

To get rid of the extraneous "type" property I added an @XmlClassExtractor annotation to the "Pet" class. Strangely, the class extractor referenced by the annotation is never called?!?!

I've read a lot of confused posts about this out there so maybe an 'official' example/test case might be a good idea?

@frantuma
Copy link
Member

frantuma commented Feb 5, 2019

One way to achieve what you need with annotations would be the following:

	@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;
        }

    }

which would result to the following spec:

openapi: 3.0.1
paths:
  /pets/cat:
    get:
      summary: Generate a cat
      operationId: cat
      responses:
        default:
          description: a cat
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Cat'
  /pets/random:
    get:
      summary: Generate a pet
      operationId: random
      responses:
        default:
          description: a pet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
components:
  schemas:
    Cat:
      required:
      - petType
      - petTypeAsString
      type: object
      properties:
        hunts:
          type: boolean
      allOf:
      - $ref: '#/components/schemas/Pet'
    Dog:
      required:
      - petType
      - petTypeAsString
      type: object
      properties:
        barks:
          type: boolean
      allOf:
      - $ref: '#/components/schemas/Pet'
    Pet:
      title: Pet
      required:
      - petType
      - petTypeAsString
      type: object
      properties:
        petType:
          type: string
          enum:
          - CAT
          - DOG
        petTypeAsString:
          type: string
      discriminator:
        propertyName: petTypeAsString
        mapping:
          CAT: '#/components/schemas/Cat'
          DOG: '#/components/schemas/Dog'

@MissedSte4k
Copy link

Hey,
@frantuma, is your example still valid to this day? I've been having some issues with polymorphism documentation in my project, then decided to test it with your pet code as you wrote in your example, but instead of getting results you got, at the endpoint, I actually get this:

 /pets/random:
     "get": {
                ......
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "*/*": {
                                "schema": {
                                    "oneOf": [
                                        {
                                            "$ref": "#/components/schemas/Pet"
                                        },
                                        {
                                            "$ref": "#/components/schemas/Cat"
                                        },
                                        {
                                            "$ref": "#/components/schemas/Dog"
                                        }
                                    ]
                                }
                            }
                        }
                    }
                },

Which is clearly wrong.

I'm useing springdoc-openapi 1.6.6
Spring 2.6.2
for generating the docs.

I already wrote an issue to springdoc-openapi with examples, hoping it's their problem and they will fix it, but they said it's not an issue on their part (and just referenced another issue that is somewhat related, but not).

@frantuma
Copy link
Member

Example is still valid with latest version:

package io.swagger.v3.jaxrs2.resources;

import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.Schema;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Random;

@Path("/test")
public class DiscriminatorResource {

    @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"
    )
    static 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();
        }

    }

    static class Cat extends Pet {
        public boolean hunts;

        public Cat() {
            super(Pet.PetType.CAT);
        }

        public Cat(boolean hunts) {
            this();
            this.hunts = hunts;
        }
    }

    static class Dog extends Pet {
        public boolean barks;

        public Dog() {
            super(Pet.PetType.DOG);
        }

        public Dog(boolean barks) {
            this();
            this.barks = barks;
        }

    }
}

resolves into:

openapi: 3.0.1
paths:
  /upload/random:
    get:
      operationId: random
      responses:
        default:
          description: default response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
components:
  schemas:
    Cat:
      required:
      - petType
      - petTypeAsString
      type: object
      allOf:
      - $ref: '#/components/schemas/Pet'
      - type: object
        properties:
          hunts:
            type: boolean
    Dog:
      required:
      - petType
      - petTypeAsString
      type: object
      allOf:
      - $ref: '#/components/schemas/Pet'
      - type: object
        properties:
          barks:
            type: boolean
    Pet:
      title: Pet
      required:
      - petType
      - petTypeAsString
      type: object
      properties:
        petType:
          type: string
          enum:
          - CAT
          - DOG
        petTypeAsString:
          type: string
      discriminator:
        propertyName: petTypeAsString
        mapping:
          CAT: '#/components/schemas/Cat'
          DOG: '#/components/schemas/Dog'

@marccollin
Copy link

is it possible to do it with an interface?

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = MoneyTransfert.class, name = "money"),
@JsonSubTypes.Type(value = BitcointTransfert.class, name = "bitcoin"),
})
public interface Transfert {
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants