Springfox: Duplicate class names in different packages get squashed in ControllerDocumentation - modelMap

Created on 4 Dec 2013  ·  50Comments  ·  Source: springfox/springfox

I haven't yet made a fix for this yet but our team encountered this issue and I believe it's fairly easy to fix but wanted to get feedback from you on how to go about this.

Example of the issue given the following controller method:

    public void sampleMethod3(com.mangofactory.swagger.spring.sample.duplicate.Pet petOne, com.mangofactory.swagger.spring.sample.Pet petTwo) {
    }

Notice that petOne and petTwo are different classes but have the same class name. When the ControllerDocumnetation modelMap is being populated it uses a friendly class name as the key so it would be "Pet" in this instance. With "Pet" being the same key in the model map they will overwrite each other.

Ways I was thinking to go about fixing this:

  1. Just add in the package name...so friendly names will now be fully qualified (Not so friendly names)
  2. Add configuration into SwaggerConfiguration that would trigger friendly/fully qualified names

I am really not sure which one to do...friendly names are nice but people might end up not realizing this is happening so it's almost safer to have fully qualified.

Not sure if you need a reason of why this is important but here is it:

  1. Service versioning - having multiple version of client model object could cause these collision to happen over time
  2. Services that return subsets of a canonical model - some data might be expensive or unnecessary to return in this case service A could return petOne (superset) and service B could return petTwo (subset of petOne).

Let me know what you think and if I have time I don't mind working on this fix as it will benefit our team.

feature wontfix

Most helpful comment

@Chinna-SHS The easiest for now is to use the @ApiModel attribute and set different values on the two.

e.g.

package v1;
@ApiModel("V1Model")
class ModelX {}

package v2;
@ApiModel("V2Model")
class ModelX {}

All 50 comments

@mbazos Interesting bug and totally an overlooked case :) Thanks for pointing it out! :+1:. Couple of observations, not to be pedantic and all :-o!

  • When you're thinking about these service interfaces, its almost a good idea not to leak internal implementation to the projected service interface.
  • Two versions of a domain shouldn't co-mingle at service boundaries, that adaptation should again be an internal implementation.

With that out of the way, there is certainly a case that has the notion of the model namespaced by package rather than by the name of the type, like in your case com.xxxxx.canonical.Pet and com.xxxxx.summary.Pet.

I would say the simplest solution, (for now) rather than bringing in the package name etc. is the create a custom annotation.

public void sampleMethod3(@ModelAlias("PetCanonical") com.xxxxxx.canonical.Pet petOne, 
                                        @ModelAlias("PetSummary") com.xxxxxx.summary.Pet petTwo) {
    }

I'd modify ResolvedTypes to handle aliasing model names.

I think its fairly simple and also doesnt clutter the names with package names etc. I'm trying to get an intermediate release with 1.2 spec support, so, If you have some cycles I'd love to take in a pr! I'm planning to pay down some tech debt in 0.7 and modularizing the library. I could create a chore to make that an extensibility point.

Let me know if you have any questions.

Thanks for getting back to me so quickly with this. So I completely agree with you on the two points you have outlined. Based on what you are saying and it completely makes sense is that I would rename my objects that are similar to give them a more distinct name.

For the example you outlined above you could have com.xxxxxx.canonical.Pet and com.xxxxxx.summary.PetSummary or com.xxxxxx.canonical.PetSummary. That way the domain objects are distinct and there is no confusion about what Pet domain object is what.

The above annotation becomes valuable when you supporting multiple versions of a given service (happens all the time because consumers might not want to move to the latest...for whatever reason). Imagine this package structure:

 com.xxxxxx.v1.canonical.Pet
 com.xxxxxx.v2.canonical.Pet

I know ideally your canonical model shouldn't change but in practice this happens I see these things happen:

  1. Business needs/rules change
  2. You learn there is a better way to represent your data for your clients
  3. Deprecate features that are no longer applicable
  4. Lazy consumers don't have "time" to upgrade

Service versioning is always an interesting topic. Again I really appreciate the help.

I have a similar situation except that this is happening on the response class as well.
We have some generated JAXB classes which unfortunately share the same top level class. Even though the classes are fully qualified, springfox seems to pick one and use that everywhere.

Example below:

@ApiOperation(value = "Save something")
public blah.blah.Document create(@RequestBody foo.bar.Document thingy) {
  ...
  return new foo.bar.Document();
}

The Swagger docs will only contain one representation of Document...

Yeah this needs to be fixed.

I'm also having the same issue. A fix would be very much appreciated!

If I have some time I might work on this. I recently upgraded our apps from swagger-springmvc to springfox and I am really liking the re-organization and re-packaging of the libraries.

I am going to be out of the country and won't be back until the last week in September, I will make sure to check here before I start any work on this.

@mbazos :metal: would be much appreciated!

However, since this feature affects the core, I would like to collaborate on how you plan to approach this before you work on the PR. Just want to make sure its in-line with the direction we want to take this library.

Let me know if I can answer any questions.

Could you please provide some aprox estimations when the issue will be fixed?

ping @mbazos!

Yes been very busy with life and work. Currently on break for the holidays will try to get this done before the new year.

Not a problem... was just pinging to see if you still had plans to help with this feature. It will be much appreciated ... Thanks!

@dilipkrish It's been a while since I have looked at this code and our discussion above references the old version of swagger-springmvc before you worked on the major refactor. Anyway I forked the repo and got my local workstation all up and running.

Just wanted to check a few things:
So I think what needs to change is DefaultTypeNameProvider, is this correct? Right now it's returning the class simple name which will conflict if more than one class have the same name.

Also I took a second look and I can see in the DefaultModelProvider the following:

  private Optional<Model> mapModel(ModelContext parentContext, ResolvedType resolvedType) {
    if (isMapType(resolvedType) && !parentContext.hasSeenBefore(resolvedType)) {
      String typeName = typeNameExtractor.typeName(parentContext);
      return Optional.of(parentContext.getBuilder()
          .id(typeName)
          .type(resolvedType)
          .name(typeName)
          .qualifiedType(ResolvedTypes.simpleQualifiedTypeName(resolvedType))
          .properties(new HashMap<String, ModelProperty>())
          .description("")
          .baseModel("")
          .discriminator("")
          .subTypes(new ArrayList<String>())
          .build());
    }
    return Optional.absent();
  }

I believe .id() needs to be the fully qualified name but .name() needs to be the simple name. Can you just let me know to make sure I am on the right path.

I believe thats sounds correct but haven't dug in recently to tell u without spelunking. Off hand I know that there is also TypeNameExtractor that uses the TypeNameProviders plugins.

I thought I'd just chime in with a few expectations.

  • It would be good to verify that your solution will work with swagger-codegen
  • I would feature toggle this feature with a flag in the docket similar to the enableUrlTemplating flag _and mark with an @Incubating annotation_.
  • Implement it in such a way that it falls back to current behavior if it doesn't find two models with the same name; similar to how we do operation unique ids using some kind of caching technique like here

I'll report back if I find anything different or interesting. Thanks for helping out!

@dilipkrish just getting comfortable with the project and I have to be honest, I am trying to setup the things you mentioned above and I am partially there but I am having a hard time understanding how everything is connected.

I think I know the code changes I need to make in DefaultModelProvider but I am having a hard time understanding what I need to do to setup the plugin so I can make the decision of when to use this new behavior or not. I am a little confused about the operation/unique ids examples you sent me.

is there an irc chat or something that some of the devs use?

This works -> Join the chat at <a href="https://gitter.im/springfox/springfox">https://gitter.im/springfox/springfox</a>

I dont blame you ... its a lot of stuff :)

Plugins are just regular spring beans, the only thing is they have an additional contract that allow the plugins to be partitioned/ filtered and queried by documentation type (swagger 1.2 or swagger 2.0).

To get familiar with how the different components work together see this architecture diagram.

Plugins are ordered and have priorities. First the springfox-spring-web plugins are applied then the springfox-swagger2 plugins are applied.

Plugins layer information on the the builders they represent, for e.g. these are all the available schema plugins in the springfox-spi module. Model builder plugin gets passed in a model context which gives access to the ModelBuilder that is used to build the model.

@dilipkrish I committed and initial push here [mbazos - springfox][https://github.com/mbazos/springfox] but I am just not sure if I am going in the right direction. Basically what I am doing is creating a new plugin for the "Id" provider the default will use the class getSimpleName() and the new one will use the class name as I believe this will cause different models to be in the modelMap regardless of the the model name (I believe this is correct based on debugging and looking through the code).

When you get a chance or if there is someone else that can help with suggestions can you please take a look to make sure I am not way off target.

Ill take a look at annotate! thanks!

@mbazos @dilipkrish - I have a similar issue where I have 2 classes with the same names, but in different packages, but swagger-ui does not show up the correct structure. Is there anyway I can qualified which class to use as part of the swagger-ui?

Say I 2 classes called legacy.Pet and modern.Pet but different package names, both Pet classes have different attributes.

Class 1:
package legacy;
public class Pet {
private String address;
private String phone;
}

Class 2:

package modern;
public class Pet {
private String name;
private String age;
}

When I try to expose this via a controller:

@RequestMapping(value = "test",
                method = RequestMethod.POST)
public ResponseEntity<modern.Pet> test(
        @RequestBody
        legacy.Pet pet) {

.....
}

Swagger UI, combines the attributes of both and displays them.

Any help would be really appreciated.

screen shot 2016-02-25 at 06 46 15

@vignesharunachalam I had the same issue lately. Until a clear solution arises, you can always go around the problem by annotating at least one of your model with @ApiModel and use a unique name (e.g. LegacyPet for instance)

I am facing same issue. Is there a workaround till we have a solution

hi, Is there any workaround for this issue. I have versioned REST api and having same class in different versioned package. It would be great if you have any workaround.

@Chinna-SHS The easiest for now is to use the @ApiModel attribute and set different values on the two.

e.g.

package v1;
@ApiModel("V1Model")
class ModelX {}

package v2;
@ApiModel("V2Model")
class ModelX {}

I have run into this as well. Any updates on progress around this is a fix still targeted for 2.7.0? Is there anything we can do to help out with a fix?

@patrickbray This is still targeted for 2.7.0 but it has a workaround as described above

We are facing the same issue and it is causing a lot of confusion to our clients. Any update on when this will be fixed or if it's already fixed ?

Thanks!

@rajatkasliwal It is not fixed currently. It is still open.

Hi Dilipkrish, You had plans of fixing this in 2.7.0 .
Any update about this as even 2.8.0 is released now.

Its not going to be in 2.9.0 (coming soon), but will be in the next major release

Thanks

I upgraded to 2.9.2 and am still seeing this issue

Also, my classes are generated by JAXB and thus cannot be modified with the ApiModel property specified in the workaround. Was this supposed to be fixed in 2.9.0?

@jrichmond4 this is closed because it is part of #2056

@dilipkrish I've checked that PR and @MaksimOrlov said that he is almost done.
Is this still planned only for 3.0.0 like you mentioned some time ago or will it be available in some 2.9.X version?

Nasty thing is, we also use swagger-codegen for typescript and it generates only the one class definition that is exposed by springfox. This only matches the actual return type of one of the controllers, for the other controllers we get typescript code that doesn't match the actual payload structure.

This is also a silent failure because springfox doesn't complain, and swagger-codegen doesn't know that something is wrong, because it only reads the api-docs. So we have to manually keep modelnames unique all across our codebase which is not that easy ;)

Best regards and thanks!

Branch with model enhancement support has been merged to master and now available in 3.0.0-SNAPSHOT version. I would be grateful for feedback of this functionality.

Hi, that's awesome!
Is there some kind of documentation on what has changed and how to use / migrate to it?

@sLite, just switch to latest snapshot version.

Same class names will be produced with indexes. F.e. If there are two classes:

fiirst.packegr.Pet;
second.packege.Pet;

that will be produces as Pet and Pet_1

@MaksimOrlov I observed that the numbering is not consistent between application startups. I have 3 models in 3 different packages which always get a varying number assigned. Is this due to the scan order? Can this be fixed at all? For documentation it's not a big deal but we're planning to generate client source code from the generated docs and if naming keeps changing between deployments the generated source code will change even tho' the DTO hasn't been modified.

@benjvoigt, numbering must be stable from run to run. If you observing numbering that being changed during a few runs, that is issue. It would be great if you make example, that reproduce problem, then I will be able to fix.

@MaksimOrlov There you go: https://github.com/benjvoigt/springfox-naming-demo
The repo uses lombok, so make sure annotation processing is enabled on compiling.

This would be my result for the first run:
/v1/mystatus-one --> Health_1
/v2/mystatus-two --> Health
/v3/mystatus-three --> Health_2

{
    "swagger": "2.0",
    "info": {
        "description": "demo-service API definitions",
        "version": "1.0",
        "title": "demo-service",
        "termsOfService": "termsOfServiceURL",
        "contact": {
            "name": "Demo",
            "url": "https://www.example.com/"
        },
        "license": {}
    },
    "host": "localhost:8080",
    "basePath": "/",
    "tags": [
        {
            "name": "controller",
            "description": "Controller"
        }
    ],
    "consumes": [
        "application/json"
    ],
    "produces": [
        "application/json"
    ],
    "paths": {
        "/v1/mystatus-one": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET_1",
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health_1"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        },
        "/v2/mystatus-two": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET",
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        },
        "/v3/mystatus-three": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET_2",
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health_2"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        }
    },
    "securityDefinitions": {
        "api_token_sec_key": {
            "type": "apiKey",
            "name": "X-API-Token",
            "in": "header"
        }
    },
    "definitions": {
        "Health": {
            "type": "object",
            "required": [
                "id",
                "otherId"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                },
                "otherId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the other"
                }
            },
            "title": "Health"
        },
        "Health_1": {
            "type": "object",
            "required": [
                "id"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                }
            },
            "title": "Health_1"
        },
        "Health_2": {
            "type": "object",
            "required": [
                "id",
                "otherId",
                "thirdId"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                },
                "otherId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the other"
                },
                "thirdId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the third"
                }
            },
            "title": "Health_2"
        }
    }
}

And the second run:
/v1/mystatus-one -->Health_2
/v2/mystatus-two --> Health_1
/v3/mystatus-three --> Health

{
    "swagger": "2.0",
    "info": {
        "description": "demo-service API definitions",
        "version": "1.0",
        "title": "demo-service",
        "termsOfService": "termsOfServiceURL",
        "contact": {
            "name": "Demo",
            "url": "https://www.example.com/"
        },
        "license": {}
    },
    "host": "localhost:8080",
    "basePath": "/",
    "tags": [
        {
            "name": "controller",
            "description": "Controller"
        }
    ],
    "consumes": [
        "application/json"
    ],
    "produces": [
        "application/json"
    ],
    "paths": {
        "/v1/mystatus-one": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET_2",
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health_2"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        },
        "/v2/mystatus-two": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET_1",
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health_1"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        },
        "/v3/mystatus-three": {
            "get": {
                "tags": [
                    "Status"
                ],
                "summary": "Get current service status",
                "operationId": "getServiceStatusUsingGET",
                "parameters": [
                    {
                        "name": "X-Authorization-Token",
                        "in": "header",
                        "description": "Authorization Token",
                        "required": false,
                        "type": "string"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "schema": {
                            "$ref": "#/definitions/Health"
                        }
                    }
                },
                "security": [
                    {
                        "api_token_sec_key": []
                    }
                ],
                "deprecated": false
            }
        }
    },
    "securityDefinitions": {
        "api_token_sec_key": {
            "type": "apiKey",
            "name": "X-API-Token",
            "in": "header"
        }
    },
    "definitions": {
        "Health": {
            "type": "object",
            "required": [
                "id",
                "otherId",
                "thirdId"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                },
                "otherId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the other"
                },
                "thirdId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the third"
                }
            },
            "title": "Health"
        },
        "Health_1": {
            "type": "object",
            "required": [
                "id",
                "otherId"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                },
                "otherId": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id of the other"
                }
            },
            "title": "Health_1"
        },
        "Health_2": {
            "type": "object",
            "required": [
                "id"
            ],
            "properties": {
                "id": {
                    "type": "string",
                    "example": "ade02824-32ba-4157-8122-807a48b13268",
                    "description": "Unique id"
                }
            },
            "title": "Health_2"
        }
    }
}

@benjvoigt, thanks, will look it in a next few days.

I am not a spring fox developer. Just interested in getting this solved.

If you are looking for a solution, you could get the full package string plus class name string and then call the hashcode function to get a number. Not a nice number but could solve your problem most of the time. It would also be scan order independent.

Just a thought and option.

Get Outlook for Androidhttps://aka.ms/ghei36


From: benjvoigt notifications@github.com
Sent: Friday, July 12, 2019 6:01:53 AM
To: springfox/springfox
Cc: robertsearle; Manual
Subject: Re: [springfox/springfox] Duplicate class names in different packages get squashed in ControllerDocumentation - modelMap (#182)

@MaksimOrlovhttps://github.com/MaksimOrlov I observed that the numbering is not consistent between application startups. I have 3 models in 3 different packages which always get a varying number assigned. Is this due to the scan order? Can this be fixed at all? For documentation it's not a big deal but we're planning to generate client source code from the generated docs and if naming keeps changing between deployments the generated source code will change even tho' the DTO hasn't been modified.


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHubhttps://github.com/springfox/springfox/issues/182?email_source=notifications&email_token=ADKWRR4GB2P635NSDN4FABTP7B6EDA5CNFSM4AKHLXI2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZZWAUQ#issuecomment-510877778, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ADKWRR6VITV6A5JFR5TJOULP7B6EDANCNFSM4AKHLXIQ.

@robertsearle, the hasCode() function is not reliable. Hashes can collide, could be two same numbers for different strings. I think the better approach is to use full method signature.

@dilipkrish Why the ticket is marked as solved? What's the solution to use full name/package name instead of simple name for the model?

Sorry, saw it was marked as solved in the release notes but not in the actual ticket

@robertsearle, @benjvoigt , the fix has been merged to master branch. Could you, please, provide a feedback for 3.0.0-SNAPSHOT, if the issue was fixed or not ?

@MaksimOrlov numbering appears to be stable between runs now, ty. I tested with multiple endpoints and also with nested models as return value.

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

This issue has been automatically closed because it has not had recent activity. Please re-open a new issue if this is still an issue.

For java, you can use the following workaround

@Primary
@Component
public class SwaggerApiModelTypeNameProvider extends ApiModelTypeNameProvider {

private final Map<String, Pair<String, String>> classNames = new ConcurrentHashMap<>();

@Override
public String nameFor(Class<?> type) {
    ApiModel annotation = AnnotationUtils.findAnnotation(type, ApiModel.class);
    String defaultTypeName = type.getTypeName().replace(type.getPackageName() + ".", "");

    defaultTypeName = annotation != null ? Optional.ofNullable(Strings.emptyToNull(annotation.value())).orElse(defaultTypeName) : defaultTypeName;

    Pair<String, String> classPackagePair = classNames.get(defaultTypeName);
    if(classPackagePair != null) {
        if(!classPackagePair.getValue().equalsIgnoreCase(type.getPackageName())) {
            defaultTypeName = "_" + defaultTypeName;
        }
    }
    classNames.put(defaultTypeName, Pair.of(type.getTypeName(), type.getPackageName()));

    return defaultTypeName;

}

}

You can modify it if needed :)

Was this page helpful?
0 / 5 - 0 ratings