swagger-api / swagger-api/swagger-core

[Feature]: Prevent name clash for identical nested class names

Open
#5,101 2 comments 2 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

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

Description

Feature Description

Duplicate class names are not resolved by the library (see #5016).
One special case of name clashes have to do with nested classes.

We ran into schema-name collisions when two top-level models each define a nested class with the same simple name.

Today the behavior seems to be effectively binary:

  • default naming uses the simple class name, for example SiteStop
  • fully qualified naming uses the package + class name, for example com.example.TripExport.SiteStop
    The first option causes collisions for nested types with the same simple name.
    The second option avoids collisions, but it exposes package names in the public OpenAPI schema, which is often more verbose than desired.

This feature is a request to include the outer name to prevent name clashes.
For example:
TripExport.SiteStop
PublishedTrip.SiteStop

Use Case

The issue would prevent name clash in this case:

package com.example.demo;

import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class DemoController {

    @GetMapping("/trip")
    public TripExport trip() {
        return null;
    }

    @GetMapping("/published-trip")
    public PublishedTrip publishedTrip() {
        return null;
    }
}

class TripExport {
    public List<SiteStop> siteStops;

    static class SiteStop {
        public String stopId;
    }
}

class PublishedTrip {
    public List<SiteStop> siteStops;

    static class SiteStop {
        public String stopId;
    }
}

Suggested Solution (optional)

The problem can we resolved (or worked around) by implementing implementing a different TypeNameResolver and registering it in a ModelResolver.

   @Bean
    public static ModelResolver createModelResolver(ObjectMapper objectMapper) {
        return new ModelResolver(objectMapper, new NestedTypeNameResolver());
    }

    static final class NestedTypeNameResolver extends TypeNameResolver {
        @Override
        protected String getNameOfClass(Class<?> cls) {
            if (cls.getEnclosingClass() == null) {
                return cls.getSimpleName();
            }

            Deque<String> names = new ArrayDeque<>();
            for (Class<?> current = cls; current != null; current = current.getEnclosingClass()) {
                names.push(current.getSimpleName());
            }
            return String.join(".", names);
        }
    }

Alternatives Considered

The fqn toggle also prevens the collision but includes the full package name in the schema, which was not desired in our case.

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 by reading TypeNameResolver and ModelResolver, then reproduce the issue with the TripExport and PublishedTrip nested SiteStop classes described in the issue. Done means nested schema names include their enclosing class names, avoid collisions, and do not expose package names; add coverage for the demonstrated case and verify the generated OpenAPI schemas.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend-api-design
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.