springfox: Duplicated enumerations in swagger definitions

Version 2.6.1

Enums are currently being listed inline in the swagger output, causing enums to be generated as inner class. They should be referenced via "$ref" like classes are. Attached are two swagger json definitions, one provided by springfox and the other fixed by hand.

This is problematic for clients that attempt to use the model files, as they cannot be reused across api endpoints, as each generated model will contain a copy of the enum. This is especially problematic in apis that define enums with large sets of values, such as an enum set of the 249 ISO 3166 alpha 2 or 3 letter country codes.

Example enum: public enum Crest { ONE, TWO, THREE, }

Example class:

public class Obj {
    private Crest crest;

    public Crest getCrest() {
        return crest;
    }

    public void setCrest(Crest crest) {
        this.crest = crest;
    }
}

Incorrect swagger:

"definitions": {
  "Obj": {
    "type": "object",
    "properties": {
      "crest": {
        "type": "string",
        "enum": [
          "ONE",
          "TWO",
          "THREE"
        ]
      }
    }
  },
}

Resulting codegen:

// ... snip ...
public class Obj {
  /**
   * Gets or Sets crest
   */
  public enum CrestEnum {
    ONE("ONE"),
    
    TWO("TWO"),
    
    THREE("THREE");

    private String value;

    CrestEnum(String value) {
      this.value = value;
    }
  }
  // ... snip ...
}

Correct swagger:

"definitions": {
  "Obj": {
    "type": "object",
    "properties": {
      "crest": {
        "$ref": "#/definitions/Crest"
      }
    }
  },
  "Crest": {
    "type": "string",
    "enum": [
      "ONE",
      "TWO",
      "THREE"
    ]
  },
}

Resulting codegen:

// ... snip ...
public enum Crest {
  
  ONE("ONE"),
  
  TWO("TWO"),
  
  THREE("THREE");

  private String value;

  Crest(String value) {
    this.value = value;
  }
  // ... snip ...
}

// ... snip ...
public class Obj {
  @JsonProperty("crest")
  private Crest crest = null;

  // ... snip ...
  public Crest getCrest() {
    return crest;
  }

  public void setCrest(Crest crest) {
    this.crest = crest;
  }
  // ... snip ...
}

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 16
  • Comments: 34 (16 by maintainers)

Commits related to this issue

Most upvoted comments

Similar use case for us: we now have a number of places where we need to cast generated TS enums, to a different instance of that same Java enum, because they’ve been generated using this class-specific naming. Would love to hear if this change is thought to be manageable

This one is a toughie 😳 Its not easy to isolate enumerations easily the way things have been structured. I’ll take a look at how I can tease out this feature incrementally so that its possible

@toddbyrne I see where you are coming from. However I contend that it is not in scope of this library.

  1. Firstly and most importantly, the following definition is just not valid json schema. Enums are not first class objects that can be defined in the definitions section.
"Crest": {
    "type": "string",
    "enum": [
      "ONE",
      "TWO",
      "THREE"
    ]
  }
  1. Secondly, even if it were valid, it is just weird when you take out the fact that the type of crest is an enum. Let me illustrate with an example.
"Obj1": {
  "type": "object",
  "properties": {
    "crest": {
      "type": "string",
      "enum": [
        "ONE",
        "TWO",
        "THREE"
      ]
    }
  }
},
"Obj2": {
  "type": "object",
  "properties": {
    "crest": {
      "type": "string"
    }
  }
}

In the above example Obj1 and Obj2 are pretty much equivalent. Just that one has crest which is any string that you provide (Obj2) and the other is a string with a fixed set of values ONE, TWO and THREE (Obj1). Semantically, no different that one having maxlength and one not having any size limit.

  1. It is not an easy problem to decide when to make a model a reference and when to inline it especially for scalar values like string.
  2. Lastly, swagger is a language agnostic representation of models. So only code-gen tools will know how to interpret and deal with it best. What is the guarantee that the representation suggested above will work great for a ruby or a clojure client code generation.

Hi, has there been any progress on this? This effects us generating TypeScript code where we can’t use namespaces (React Native), meaning that in order to prevent name clashes in the reexport we need to prepend the class name to each enum declaration.

If you think this is now doable then I’m happy to have a look, but there is quite a lot to learn, so I want to make sure that it’s achievable before I commit the time.

Is there any updates after 3.0 released half a year?

Any updates on this now that 3.0.0 is out?

I am also struggling with this issue in a commercial production setting after reverse generating model classes from Swagger. The duplicate enums does cause unnecessary code in client systems.

  1. It is valid schema. You can verify it on http://editor.swagger.io/#!/ using my attached swagger json files, which can be used to generate clients using swagger-codegen. Verify it against what springfox currently generates.
  2. The name Crest isn’t special, it’s just a named enum. I could have just as easily named it Numbers.
  3. If the enum is not an internal class, it should always be a ref.
  4. The ruby client has no problems being generated from the corrected swagger definition files. Ruby definition of the enum below
module SwaggerClient
  class Crest
    ONE = "ONE".freeze
    TWO = "TWO".freeze
    THREE = "THREE".freeze
  end
end

Also clojure appears to be a moot point because no model files are generated regardless.

@dilipkrish How is the codegen supposed to de-duplicate enums? Having spring fox regenerate the same enum multiple times is incorrect. If you have three objects that reference the same enum then spring fox generates three enums.

@Sineaggi Hey this is perfectly valid swagger. The swagger-codegen is incorrect in its rendering of the enum.

I’ve just dropped io.springfox in favour of org.springdoc. Lack of enum refs is unacceptable in my case. I’ve just replaced

	implementation "io.springfox:springfox-boot-starter:3.0.0"

with

	implementation 'io.swagger:swagger-annotations:1.6.3'
	implementation 'io.swagger.core.v3:swagger-annotations:2.1.11'
	implementation 'org.springdoc:springdoc-openapi-ui:1.5.10'

additionally i’ve tweaked some settings in my application.yml, added bean to describe my API and I finally got enums as refs!!!

If some of your enums are still inlined (it was in my case) then add following to you Application class:

static {
    io.swagger.v3.core.jackson.ModelResolver.enumsAsRef = true;
}

source: https://github.com/springdoc/springdoc-openapi/issues/232#issuecomment-748607672

@Knut-Fjellheim understandable. I should perhaps be easier to fix once we support oas 3.x

@dilipkrish, oh, now i remember one restriction that i found out much later after implementing enum references - enum references are valid in types (for example http request or response body has enum field), but not in method parameters - so You are right that Your example api-doc (that references enums from http GET method parameter) isn’t passing validation, but that doesn’t mean that springfox shouldn’t support enum references in all other cases where enum references are allowed.

Just to clarify for future posters. The reason this is difficult

//...
  '/test/{id}':
    get:
      tags:
        - api-controller
      summary: two
      operationId: two_1
      consumes:
        - application/json
      produces:
        - '*/*'
      parameters:
        - name: id
          in: path
          description: id
          required: true
          type: string
          enum:
            - ONE
            - TWO
            - THREE
        - name: test
          in: query
          description: id
          required: true
          schema:
            $ref: '#/definitions/Crest'           
      responses:
        '200':
          description: OK
          schema:
            type: string
definitions:
  Crest:
    type: string
    enum:
      - ONE
      - TWO
      - THREE
//...

That definitions of enums as a reference is not valid. It will be inlined.

Hi, @dilipkrish. Haven’t heard Your reply - I hope You are still live and kicking 😉

As a first step forward to allow implementing enum references (as proposed by this issue), i created a PR that makes 4 classes extendable - this was enough for me to implement enum references in my own project. It contains really trivial and unharmful changes with tests - it would be fantastic, if that PR could be accepted. Then i wouldn’t have to use custom build of this library with the patches included in the PR.

As a next step I’d like to contribute enum references implementation back to this project - but i guess creating/accepting that PR could take much more time. I’d appreciate Your thoughts on it.

@dilipkrish, i’ve implemented what @Sineaggi proposed - instead of inline enum declarations, referencing them using ref. For now i patched some classes locally (for now, to make them extendable, so i could replace some beans that make it possible to create enum refs - to avoid delays in my work related to the PR i’d like to make). I’d like to contribute it back, but i have a question. I think i shouldn’t just change the schema generation, but allow the developer to decide if it should be generated using enum references. For that purpose i could add if statements to the code to check if enum references are desired. I guess i could introduce new spring configuration property (smth like springfox.schema.enumrefs=true) to allow generating enum references. But first of all i suspect You could come up with better name and secondly, i’m not sure if i should inject it to all classes that should use it, or should i create a configuration properties class (didn’t find one so far) and inject that instead.

@dilipkrish, I’d appreciate if You could add Your thoughts before i start working on the PR. PS. The classes involved with this change are DefaultModelDependencyProvider, DefaultModelProvider, TypeNameExtractor and ModelMapper.