swagger-core icon indicating copy to clipboard operation
swagger-core copied to clipboard

Incorrect mapping of sub-classes when nested as member of another sub-class

Open jochenunger opened this issue 2 years ago • 3 comments

Note: This issue was posted in springdoc-openapi first. It was closed, since the issue seams to be located in swagger-core.

Describe the bug

  • Inherited sub-classes are not mapped correctly if they are nested in a sub-class that by itself is derived from a super class.
  • I'd expect the mapping to be onOf() which is true for the nested class itself, but not for the member.
  • See the example to make things clear:

To Reproduce Steps to reproduce the behavior:

  • Spring Boot starter parent, version 3.0.2
  • Spring Doc modules starter-webmvc-ui and starter-common in version 2.0.2.

Here's an example controller:

@RestController
@RequestMapping(path = "/v1/test", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestOpenApiError {

    @GetMapping("/a")
    public A testGetA() {
        return null;
    }

    @GetMapping("/b")
    public B testGetB() {
        return null;
    }

    @GetMapping("/c")
    public C testGetC() {
        return null;
    }

    @Schema(
            description = "Some out class",
            subTypes = { A1.class, A2.class, },
            discriminatorProperty = "@type",
            discriminatorMapping = {
                    @DiscriminatorMapping(schema = A1.class, value = "A1"),
                    @DiscriminatorMapping(schema = A2.class, value = "A2"),
            }
    )
    public class A {}
    public class A1 extends A {}
    public class A2 extends A {
        public List<? extends B> listOfBs;
    }

    @Schema(
            description = "Some nested class",
            subTypes = { B1.class, B2.class, },
            discriminatorProperty = "@type",
            discriminatorMapping = {
                    @DiscriminatorMapping(schema = B1.class, value = "B1"),
                    @DiscriminatorMapping(schema = B2.class, value = "B2"),
            }
    )
    public class B {}
    public class B1 extends B {}
    public class B2 extends B {}

    public class C {
        public List<? extends B> listOfBs;
    }
}

I have three classes A, B, and C. A1 and A2 are derived from A. B1 and B2 are derived from B.

B as list is member of A2 (nested in a class that is derived, too) and B as list is member of C (not nested in an inheritance)

This is the generated outcome fragment for this as yml

B:
      type: object
      description: Some nested class
      discriminator:
        propertyName: '@type'
        mapping:
          B1: '#/components/schemas/B1'
          B2: '#/components/schemas/B2'
    B1:
      type: object
      allOf:
        - $ref: '#/components/schemas/B'
    B2:
      type: object
      allOf:
        - $ref: '#/components/schemas/B'
    C:
      type: object
      properties:
        listOfBs:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/B'
              - $ref: '#/components/schemas/B1'
              - $ref: '#/components/schemas/B2'
    A:
      type: object
      description: Some out class
      discriminator:
        propertyName: '@type'
        mapping:
          A1: '#/components/schemas/A1'
          A2: '#/components/schemas/A2'
    A1:
      type: object
      allOf:
        - $ref: '#/components/schemas/A'
    A2:
      type: object
      allOf:
        - $ref: '#/components/schemas/A'
        - type: object
          properties:
            listOfBs:
              type: array
              items:
                $ref: '#/components/schemas/B'

When B is member of C the generation is as expected. Is maps to oneOf. As expected:

    C:
      type: object
      properties:
        listOfBs:
          type: array
          items:
            oneOf:
              - $ref: '#/components/schemas/B'
              - $ref: '#/components/schemas/B1'
              - $ref: '#/components/schemas/B2'

When B is member of A2 it just refers to an array of type B. No oneOf mapping:

    A2:
      type: object
      allOf:
        - $ref: '#/components/schemas/A'
        - type: object
          properties:
            listOfBs:
              type: array
              items:
                $ref: '#/components/schemas/B'

Expected behavior I'd expect A2 to me mapped like this:

    A2:
      type: object
      allOf:
        - $ref: '#/components/schemas/A'
        - type: object
          properties:
            listOfBs:
              type: array
              items:
                oneOf:
                  - $ref: '#/components/schemas/B'
                  - $ref: '#/components/schemas/B1'
                  - $ref: '#/components/schemas/B2'

Maybe I'm doing something wrong. Maybe somebody can confirm, that this is a real issue. Thx for any help.

jochenunger avatar Feb 06 '23 08:02 jochenunger

I came across this issue searching for something similar. In my case B is abstract and I wanted to show oneOf B1 and B2. I was able to show the correct options this by adding this @ArraySchema

public class A2 extends A {
    @ArraySchema(schema = @Schema(implementation = Objects.class, oneOf = {B1.class, B2.class}))
    public List<? extends B> listOfBs;
  }

felzan avatar Sep 15 '23 22:09 felzan

I came across this issue searching for something similar. In my case B is abstract and I wanted to show oneOf B1 and B2. I was able to show the correct options this by adding this @ArraySchema

public class A2 extends A {
    @ArraySchema(schema = @Schema(implementation = Objects.class, oneOf = {B1.class, B2.class}))
    public List<? extends B> listOfBs;
  }

That's the best solution, but you have to remember, that fields from parent abstract class won't be included in docs.

mkarczewski85 avatar May 13 '24 09:05 mkarczewski85