Skip to content

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

@tbvh

Description

@tbvh

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions