OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.49k stars 6.5k forks source link

[BUG][spring] HATEAOS is not compatible with main openapi objective #13577

Open plblueraven opened 2 years ago

plblueraven commented 2 years ago

Bug Report Checklist

Description

Main openapi objective described here: https://spec.openapis.org/oas/v3.1.0.html#abstract includes:

The OpenAPI Specification (OAS) defines a standard, programming language-agnostic interface description for HTTP APIs, which allows both humans and computers to discover and understand the capabilities of a service without requiring access to source code, additional documentation, or inspection of network traffic. [...]

But if we decide to use spring generator and opt-in hateoas things get complicated.

If we go with basic definition of API spec (simplified pet store) & config - placed below. Then we got API that compiles 🥳, but at the same time is undiscoverable 😭.

Because if we do "implement" findPetsByStatus like this return ResponseEntity.ok().body(List.of(new Pet())); then such request: curl -X GET -i 'http://localhost:8080/pet/findByStatus?status=available' returns [{"links":[],"id":null,"name":null,"category":null,"photoUrls":[],"tags":null,"status":null}].

(yeah I know this is NOT HAL representation, but probably can be changed by spring configuration) Now we have some part of return value (links) not documented in openapi. But as we like api-first approach and generating client and server from one definition then only one option remains. Add Links to definition:

just add:

        links:
          $ref: '#/components/schemas/Links'

to pet properties, and components:

    Link:
      description: ''
      type: string
    Links:
      description: ''
      type: object
      properties:
        next:
          $ref: '#/components/schemas/Link'
          description: ''

And we will get:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.10.1:compile (default-compile) on project test: Compilation failure
[ERROR] tmp/pet/src/main/java/com/test/model/Pet.java:[237,23] getLinks() in com.test.model.Pet cannot override getLinks() in org.springframework.hateoas.RepresentationModel
[ERROR]   return type @javax.validation.Valid com.test.model.Links is not compatible with org.springframework.hateoas.Links

If I understand correctly here we should be rescued by https://openapi-generator.tech/docs/usage/#type-mappings-and-import-mappings

but when we add to config:

importMappings:
  Links: "org.springframework.hateoas.Links"

We ended up with API that compiles 🥳 and is discoverable 🥳. Sadly 😭now we have

public class Pet extends RepresentationModel<Pet>  {
//[...]
  @JsonProperty("links")
  private Links links;

generated as Pet model but RepresentationModel has itself:

private final List<Link> links;

Now "builder" API of Pet mixes up with RepresentationModel API and nothing is straightforward for now on. Even worse RepresentationModel API is useless as its property (links) won't end up in JSON.

openapi-generator version

Both docker's v6.1.0 and latest (master)

OpenAPI declaration file content or url

https://gist.github.com/plblueraven/9d844001829d3548f62990cd41a24392 - basic https://gist.github.com/plblueraven/f1e0481427ae99f980e6d96d963ce845 - with links

Generation Details

Generate server using docker run --rm -v "${PWD}/${generatedApiDirectoryRelativeLocation}:/generated-api" -v "${PWD}/${apiDefinitionFileRelativeLocation}:/spec/api.yaml" -v "${PWD}/${apiGeneratorConfigFileRelativeLocation}:/config.yaml" openapitools/openapi-generator-cli:latest generate -c /config.yaml

With such (effectively) config.yaml:

inputSpec: /spec/api.yaml
outputDir: generated-api
generatorName: spring
additionalProperties:
  apiPackage: com.test
  modelPackage: com.test.model
  supportingFilesToGenerate: "ApiUtil.java"
  groupId: com.test
  artifactId: test
  artifactVersion: 0.0.0
  hateoas: true

or this one:

inputSpec: /spec/api.yaml
outputDir: generated-api
generatorName: spring
importMappings:
  Links: "org.springframework.hateoas.Links"
additionalProperties:
  apiPackage: com.test
  modelPackage: com.test.model
  supportingFilesToGenerate: "ApiUtil.java"
  groupId: com.test
  artifactId: test
  artifactVersion: 0.0.0
  hateoas: true
Steps to reproduce

Generate server with above config and later go to generated directory and run mvn spring-boot:run.

Related issues/PRs

1130

Suggest a fix

Make hateoas option in spring generator compatible with main openapi objective? IMO way that it should be done needs discussion.

OssamaMACHTA commented 6 months ago

Did there is any news about this topic ?

leccyril commented 5 months ago

for me i just activate hateoas on spring generaor plugin, but even input have the link that i dont want !, i want only for ouptut object

OssamaMACHTA commented 5 months ago

From my side i find a workaround by defining a custom "pojo.mustache" template using Spring template as a base.

I added a new "vendorExtensions.x-custom-hateoas-link" balise after the "vars" ones:

/**
* {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}}
    * @deprecated{{/isDeprecated}}
*/
{{>additionalModelTypeAnnotations}}
{{#description}}
    {{#isDeprecated}}
        @Deprecated
    {{/isDeprecated}}
    {{#swagger1AnnotationLibrary}}
        @ApiModel(description = "{{{description}}}")
    {{/swagger1AnnotationLibrary}}
    {{#swagger2AnnotationLibrary}}
        @Schema({{#name}}name = "{{name}}", {{/name}}description = "{{{description}}}"{{#deprecated}}, deprecated = true{{/deprecated}})
    {{/swagger2AnnotationLibrary}}
{{/description}}
{{#discriminator}}
    {{>typeInfoAnnotation}}
{{/discriminator}}
{{#jackson}}
    {{#isClassnameSanitized}}
        {{^hasDiscriminatorWithNonEmptyMapping}}
            @JsonTypeName("{{name}}")
        {{/hasDiscriminatorWithNonEmptyMapping}}
    {{/isClassnameSanitized}}
{{/jackson}}
{{#withXml}}
    {{>xmlAnnotation}}
{{/withXml}}
{{>generatedAnnotation}}
{{#vendorExtensions.x-class-extra-annotation}}
    {{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}{{^parent}}{{#hateoas}} extends RepresentationModel<{{classname}}> {{/hateoas}}{{/parent}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#serializableModel}}

    private static final long serialVersionUID = 1L;
{{/serializableModel}}
{{#vars}}
    {{^vendorExtensions.x-custom-hateoas-link}}
        {{#isEnum}}
            {{^isContainer}}
                {{>enumClass}}
            {{/isContainer}}
            {{#isContainer}}
                {{#mostInnerItems}}
                    {{>enumClass}}
                {{/mostInnerItems}}
            {{/isContainer}}
        {{/isEnum}}
        {{#gson}}
            @SerializedName("{{baseName}}")
        {{/gson}}
        {{#lombok.RequiredArgsConstructor}}
            {{^useBeanValidation}}
                {{#required}}
                    @lombok.NonNull
                {{/required}}
            {{/useBeanValidation}}
        {{/lombok.RequiredArgsConstructor}}
        {{#lombok.ToString}}
            {{#isPassword}}
                @lombok.ToString.Exclude
            {{/isPassword}}
        {{/lombok.ToString}}
        {{#vendorExtensions.x-field-extra-annotation}}
            {{{vendorExtensions.x-field-extra-annotation}}}
        {{/vendorExtensions.x-field-extra-annotation}}
        {{#deprecated}}
            @Deprecated
        {{/deprecated}}

        {{#isContainer}}
            {{#useBeanValidation}}@Valid{{/useBeanValidation}}
            {{#openApiNullable}}
                private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}{{#required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/isNullable}}{{/required}}
            {{/openApiNullable}}
            {{^openApiNullable}}
                private {{>nullableDataType}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};
            {{/openApiNullable}}
        {{/isContainer}}
        {{^isContainer}}
            {{#isDate}}
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
            {{/isDate}}
            {{#isDateTime}}
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
            {{/isDateTime}}
            {{#openApiNullable}}
                private {{#isNullable}}{{>nullableDataTypeBeanValidation}} {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined();{{/isNullable}}{{^required}}{{^isNullable}}{{>nullableDataTypeBeanValidation}} {{name}}{{#useOptional}} = Optional.{{^defaultValue}}empty(){{/defaultValue}}{{#defaultValue}}of({{{.}}}){{/defaultValue}};{{/useOptional}}{{^useOptional}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/useOptional}}{{/isNullable}}{{/required}}{{^isNullable}}{{#required}}{{>nullableDataTypeBeanValidation}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}};{{/required}}{{/isNullable}}
            {{/openApiNullable}}
            {{^openApiNullable}}
                private {{>nullableDataType}} {{name}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/isNullable}};
            {{/openApiNullable}}
        {{/isContainer}}
    {{/vendorExtensions.x-custom-hateoas-link}}
{{/vars}}

{{^lombok.Data}}
    {{^lombok.RequiredArgsConstructor}}
        {{#generatedConstructorWithRequiredArgs}}
            {{#hasRequired}}

                {{^lombok.NoArgsConstructor}}
                    public {{classname}}() {
                    super();
                    }

                {{/lombok.NoArgsConstructor}}
                /**
                * Constructor with only required parameters
                */
                public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) {
                {{#parent}}
                    super({{#parentRequiredVars}}{{name}}{{^-last}}, {{/-last}}{{/parentRequiredVars}});
                {{/parent}}
                {{#vars}}
                    {{^vendorExtensions.x-custom-hateoas-link}}

                    {{#required}}
                        {{#openApiNullable}}
                            this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.ofNullable({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}};
                        {{/openApiNullable}}
                        {{^openApiNullable}}
                            this.{{name}} = {{name}};
                        {{/openApiNullable}}
                    {{/required}}

                    {{/vendorExtensions.x-custom-hateoas-link}}
                {{/vars}}
                }
            {{/hasRequired}}
        {{/generatedConstructorWithRequiredArgs}}
    {{/lombok.RequiredArgsConstructor}}
{{/lombok.Data}}
{{#vars}}
    {{^vendorExtensions.x-custom-hateoas-link}}

        {{^lombok.Data}}
        {{! begin feature: fluent setter methods }}
            public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
            {{#openApiNullable}}
                this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional.of({{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{name}}{{#isNullable}}){{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}){{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}};
            {{/openApiNullable}}
            {{^openApiNullable}}
                this.{{name}} = {{name}};
            {{/openApiNullable}}
            return this;
            }
            {{#isArray}}

                public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
                {{#openApiNullable}}
                    if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
                    this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}};
                    }
                    this.{{name}}{{#isNullable}}.get(){{/isNullable}}.add({{name}}Item);
                {{/openApiNullable}}
                {{^openApiNullable}}
                    if (this.{{name}} == null) {
                    this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}};
                    }
                    this.{{name}}.add({{name}}Item);
                {{/openApiNullable}}
                return this;
                }
            {{/isArray}}
            {{#isMap}}

                public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
                {{#openApiNullable}}
                    if (this.{{name}} == null{{#isNullable}} || !this.{{name}}.isPresent(){{/isNullable}}) {
                    this.{{name}} = {{#isNullable}}JsonNullable.of({{/isNullable}}{{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}HashMap{{/uniqueItems}}<>(){{/defaultValue}}{{#isNullable}}){{/isNullable}};
                    }
                    this.{{name}}{{#isNullable}}.get(){{/isNullable}}.put(key, {{name}}Item);
                {{/openApiNullable}}
                {{^openApiNullable}}
                    if (this.{{name}} == null) {
                    this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}HashMap{{/uniqueItems}}<>(){{/defaultValue}};
                    }
                    this.{{name}}.put(key, {{name}}Item);
                {{/openApiNullable}}
                return this;
                }
            {{/isMap}}
            {{! end feature: fluent setter methods }}
            {{! begin feature: getter and setter }}
            {{^lombok.Getter}}

                /**
                {{#description}}
                    * {{{.}}}
                {{/description}}
                {{^description}}
                    * Get {{name}}
                {{/description}}
                {{#minimum}}
                    * minimum: {{.}}
                {{/minimum}}
                {{#maximum}}
                    * maximum: {{.}}
                {{/maximum}}
                * @return {{name}}
                {{#deprecated}}
                    * @deprecated
                {{/deprecated}}
                */
                {{#vendorExtensions.x-extra-annotation}}
                    {{{vendorExtensions.x-extra-annotation}}}
                {{/vendorExtensions.x-extra-annotation}}
                {{#useBeanValidation}}
                    {{>beanValidation}}
                {{/useBeanValidation}}
                {{^useBeanValidation}}
                    {{#required}}@NotNull{{/required}}
                {{/useBeanValidation}}
                {{#swagger2AnnotationLibrary}}
                    @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}{{#deprecated}}, deprecated = true{{/deprecated}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}})
                {{/swagger2AnnotationLibrary}}
                {{#swagger1AnnotationLibrary}}
                    @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")
                {{/swagger1AnnotationLibrary}}
                {{#jackson}}
                    @JsonProperty("{{baseName}}")
                    {{#withXml}}
                        @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{.}}", {{/xmlNamespace}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}")
                    {{/withXml}}
                {{/jackson}}
                {{#deprecated}}
                    @Deprecated
                {{/deprecated}}
                public {{>nullableDataTypeBeanValidation}} {{getter}}() {
                return {{name}};
                }
            {{/lombok.Getter}}

            {{^lombok.Setter}}
                {{#deprecated}}
                    /**
                    * @deprecated
                    */
                {{/deprecated}}
                {{#vendorExtensions.x-setter-extra-annotation}}
                    {{{vendorExtensions.x-setter-extra-annotation}}}
                {{/vendorExtensions.x-setter-extra-annotation}}
                {{#deprecated}}
                    @Deprecated
                {{/deprecated}}
                public void {{setter}}({{>nullableDataType}} {{name}}) {
                this.{{name}} = {{name}};
                }
            {{/lombok.Setter}}
        {{/lombok.Data}}
    {{! end feature: getter and setter }}

    {{/vendorExtensions.x-custom-hateoas-link}}
{{/vars}}
{{>additionalProperties}}
{{^lombok.Data}}
    {{#parentVars}}

        {{^lombok.Setter}}
        {{! begin feature: fluent setter methods for inherited properties }}
            public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) {
            super.{{name}}({{name}});
            return this;
            }
            {{#isArray}}

                public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) {
                super.add{{nameInPascalCase}}Item({{name}}Item);
                return this;
                }
            {{/isArray}}
            {{#isMap}}

                public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) {
                super.put{{nameInPascalCase}}Item(key, {{name}}Item);
                return this;
                }
            {{/isMap}}
        {{/lombok.Setter}}
    {{! end feature: fluent setter methods for inherited properties }}
    {{/parentVars}}

    {{^lombok.ToString}}
            @Override
            public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("class {{classname}} {\n");
        {{#parent}}
                sb.append("    ").append(toIndentedString(super.toString())).append("\n");
        {{/parent}}
        {{#vars}}{{^vendorExtensions.x-custom-hateoas-link}}
                sb.append("    {{name}}: ").append({{#isPassword}}
                    "*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).append("\n");
        {{/vendorExtensions.x-custom-hateoas-link}}{{/vars}}{{#additionalPropertiesType}}
                sb.append("    additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n");
        {{/additionalPropertiesType}}sb.append("}");
            return sb.toString();
            }

            /**
            * Convert the given object to string with each line indented by 4 spaces
            * (except the first line).
            */
            private String toIndentedString(Object o) {
            if (o == null) {
            return "null";
            }
            return o.toString().replace("\n", "\n    ");
            }
    {{/lombok.ToString}}
{{/lombok.Data}}
    }

I also removed the "lombok.EqualsAndHashCode" parts and simply added the lombok annotation "@lombok.EqualsAndHashCode(callSuper = true)" by using the "configOptions.set([additionalModelTypeAnnotations : "@lombok.Builder @lombok.EqualsAndHashCode(callSuper = true)])" provided by the gradle OpenAPI generator plugin.

Then in my OpenAPI documentation i added the "x-custom-hateoas-link: true" parameter to the "_links" objects presents in the schemas section like that :

  schemas:
    Context:
      type: object
      description: Context response object
      required:
        - "_embedded"
        - "_links"
      properties:
        "_embedded":
          type: object
          readOnly: true
          required:
            - default_data
          properties:
            default_data:
              $ref: "#/components/schemas/DefaultData"
        "_links":
          x-custom-hateoas-link: true
          type: object
          readOnly: true
          required:
            - self
          properties:
            self:
              description: Link to this resource
              type: object
              required:
                - href
              properties:
                href:
                  type: string
                  example: 'uri to your resource'

I also made other configurations to avoid the generation of the "ContextLinks" class by using the ".openapi-generator-ignore" configuration file and used the "spotless" gradle plugin to remove the unused imports which was causing compilation error because the classes wasn't anymore generated ...

I didn't find a way to use the model.mustache template to do not generate the imports of the classes that was specified in the ".openapi-generator-ignore" configuration file ...

Here is the configuration of the OpenAPI gradle plugin :

openApiGenerate {
    // https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SpringCodegen.java
    generatorName.set("spring")
    inputSpec.set("$rootDir/src/main/resources/api/openapi.yml")
    outputDir.set(layout.buildDirectory.file("generated").get().toString())
    modelPackage.set("you.package.generated.adapter.rest.openapi")

    // https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
    templateDir.set("$rootDir/src/main/resources/api/templates")
    ignoreFileOverride.set("$rootDir/src/main/resources/api/templates/.openapi-generator-ignore")

    // https://openapi-generator.tech/docs/globals/
    globalProperties.set([verbose  : "false",
                          apis     : "false",
                          apiTests : "false",
                          invokers : "false",
                          modelDocs: "false",
                          models   : ""
    ])

    // https://openapi-generator.tech/docs/generators/spring
    configOptions.set([additionalModelTypeAnnotations          : "@lombok.Builder @lombok.EqualsAndHashCode(callSuper = true) @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)",
                       dateLibrary                             : "java8",
                       disallowAdditionalPropertiesIfNotPresent: "false",
                       openApiNullable                         : "false",
                       useSwaggerUI                            : "false",
                       useOptional                             : "true",
                       useSpringBoot3                          : "true",
                       documentationProvider                   : "none",
                       generatedConstructorWithRequiredArgs    : "false",
                       serializableModel                       : "true",
                       hateoas                                 : "true"])

    modelNameSuffix.set("Model")
}

And the configuration of the "spotless" plugin :

spotless {
    java {
        target "build/generated/src/main/java/**"
        removeUnusedImports()
    }
}

compileJava {
    sourceSets.main.java.srcDirs += layout.buildDirectory.file("generated/src/main/java").get().toString()
}

tasks.spotlessJava.dependsOn(tasks.openApiGenerate)
compileJava.dependsOn(tasks.spotlessJavaApply)

The explanation can be confusing so if you have questions, do not hesitate :)

Here are the resources that helped me to set up this workaround :

And voilà, have fun !🎉

leccyril commented 5 months ago

.openapi-generator-ignore

what it fix?

OssamaMACHTA commented 5 months ago

.openapi-generator-ignore

what it fix?

This is not a mandatory configuration to be able to use "links" named fields in the OpenAPI specification.

But this is to avoid the generation of useless classes which are created by the OpenAPI generator because the "_links" field is an object:

        "_links":
          x-custom-hateoas-link: true
          type: object

Here is the content of my ".openapi-generator-ignore" file :

# OpenAPI Generator Ignore

# Exclude HATEOAS Links related classes
**LinksModel.java
**LinksSelfModel.java
**LinksFirstModel.java
**LinksLastModel.java
**LinksNextModel.java
**LinksPrevModel.java
leccyril commented 5 months ago

mmm so you dont use the true that create the extends extends RepresentationModel ? and make it all manually ? because links are implicit, for me the issue also i have the swagger be used by customer i dont want them to see the "Link" object, if they generate for their own module this configuration will not work,

there is no issue to choose what component object we want to extends Representation model ?

like add conf ? (thanks for your conf with this i was able to add jackson annotations and on serialization at least the links disappeared)