swagger-api / swagger-api/swagger-core

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

Open
#4,358 3 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

backlog
Dominant language
Java
Stars
7.5k
Forks
2.3k
Avg merge
18h 1m
Merged PRs (30d)
10

Description

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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the TestOpenApiError reproducer and generate the OpenAPI YAML for the C and A2 cases. Compare the listOfBs schemas, using the expected A2 output as the completion criterion: the nested member should emit the same oneOf mapping as C.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, openapi, spring-boot
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.