Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG][JAVA] Since 6.5.0 Generated Lists no longer default to empty List but null #15891

Closed
5 of 6 tasks
kinsleyT opened this issue Jun 21, 2023 · 23 comments
Closed
5 of 6 tasks

Comments

@kinsleyT
Copy link

kinsleyT commented Jun 21, 2023

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator (example)?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

For nullable arrays, our generated POJOs in 6.5.0 (and 6.6.0) no longer generate initialized to an empty ArrayList (e.g private @Valid List<String> options = new ArrayList<>(); ), but instead are generated just as a non-initialized private variable private @Valid List<String> options;

Attempts to use containerDefaultToNull: 'false', and default: [] to hack our way through are not working: The first has no noticeable effect, and the second fails due to the generator failing to import the java.utils.Arrays package.

openapi-generator version

Using 6.4.0, breaks in 6.5.0 and 6.6.0

OpenAPI declaration file content or url
title: AttributeDto
properties:
    blah:
        type: string
        format: uuid
    ....
    options:
        description: |
            description
        type: array
        items:
            type: string
required:
    - blah
Generation Details

./gradlew generateApi

task cleanGeneratedApi (type: Delete) {
delete fileTree("${project.buildDir}/generated/api")
}

def openApiSpec = "${project.projectDir}/src/main/resources/api.yml"
// Generate REST API specification
task generateApi( type: org.openapitools.generator.gradle.plugin.tasks.GenerateTask ) {
inputSpec = openApiSpec
outputDir = "${project.buildDir}/generated/api"
templateDir = "${rootDir}/openapi"
invokerPackage = 'com.api'
apiPackage = 'com.api'
modelPackage = 'com.model'
generatorName = 'jaxrs-spec'
configOptions = [
interfaceOnly : 'true',
returnResponse : 'true',
useTags: 'true'
]
// Represent date-time objects as Timestamp rather than Date
typeMappings = [Date : "Timestamp"]
importMappings = ["java.util.Date" : "java.sql.Timestamp"]

dependsOn(cleanGeneratedApi)

}

Steps to reproduce
  1. Model with attribute type of array, jaxrs-spec generator, configOptions as defined above
  2. Run the gradle generate task with ./gradlew generateApi
  3. Attempting to use obj.getOptions() returns a null value instead of an empty list as before
Related issues/PRs

Potentially related? #14583
#14959
#14961

Suggest a fix

Check if the property's type is array, initialize it with an empty ArrayList, as before.

@wing328
Copy link
Member

wing328 commented Jun 22, 2023

can you please try the latest master? we recently merged a fix.

@kinsleyT
Copy link
Author

This still fails to generate unfortunately: We still get

  private @Valid List<String> options;
...


  /**
   * A list of options only applicable if the attributeType is a List. 
   **/
  public AttributeDto options(List<String> options) {
    this.options = options;
    return this;
  }

  
  @ApiModelProperty(value = "A list of options only applicable if the attributeType is a List. ")
  @JsonProperty("options")
  public List<String> getOptions() {
    return options;
  }

  @JsonProperty("options")
  public void setOptions(List<String> options) {
    this.options = options;
  }

  public AttributeDto addOptionsItem(String optionsItem) {
    if (this.options == null) {
      this.options = new ArrayList<>();
    }

    this.options.add(optionsItem);
    return this;
  }

  public AttributeDto removeOptionsItem(String optionsItem) {
    if (optionsItem != null && this.options != null) {
      this.options.remove(optionsItem);
    }

    return this;
  }

We'd want to either initialize to an empty List or when attempting to get or set initialize that list and check for its existence for parity with past behavior

@wing328
Copy link
Member

wing328 commented Jun 23, 2023

can you please share a minimal spec to reproduce the issue?

@kinsleyT
Copy link
Author

kinsleyT commented Jun 23, 2023

Spec used

openapi: 3.0.3

info:
    title: Test API
    version: 1.0.0

paths:
    /test:
         post:
            summary: Test
            operationId: test
            requestBody:
                content:
                    application/json:
                        schema:
                            type: array
                            items:
                                $ref: '#/components/schemas/TestSchema'
            responses:
              '200':
                description: Created
components:
    schemas:
        TestSchema:
            type: object
            properties:
              options:
                description: |
                    A list of options
                type: array
                items:
                    type: string

Output model file:

package org.openapitools.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.*;
import javax.validation.Valid;

import io.swagger.annotations.*;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonTypeName;



@JsonTypeName("TestSchema")
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2023-06-23T15:43:33.853525-07:00[America/Los_Angeles]")
public class TestSchema   {
  private @Valid List<String> options;

  /**
   * A list of options 
   **/
  public TestSchema options(List<String> options) {
    this.options = options;
    return this;
  }

  
  @ApiModelProperty(value = "A list of options ")
  @JsonProperty("options")
  public List<String> getOptions() {
    return options;
  }

  @JsonProperty("options")
  public void setOptions(List<String> options) {
    this.options = options;
  }

  public TestSchema addOptionsItem(String optionsItem) {
    if (this.options == null) {
      this.options = new ArrayList<>();
    }

    this.options.add(optionsItem);
    return this;
  }

  public TestSchema removeOptionsItem(String optionsItem) {
    if (optionsItem != null && this.options != null) {
      this.options.remove(optionsItem);
    }

    return this;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    TestSchema testSchema = (TestSchema) o;
    return Objects.equals(this.options, testSchema.options);
  }

  @Override
  public int hashCode() {
    return Objects.hash(options);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class TestSchema {\n");
    
    sb.append("    options: ").append(toIndentedString(options)).append("\n");
    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    ");
  }


}

Generated off latest master using command line: java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g jaxrs-spec -i ... etc

Example when generated off of 6.4.0 gradle plugin

package org.openapitools.model;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.*;
import javax.validation.Valid;

import io.swagger.annotations.*;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonTypeName;



@JsonTypeName("TestSchema")
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2023-06-23T15:56:21.770244-07:00[America/Los_Angeles]")
public class TestSchema   {
  private @Valid List<String> options = new ArrayList<>();

  /**
   * A list of options 
   **/
  public TestSchema options(List<String> options) {
    this.options = options;
    return this;
  }

  
  @ApiModelProperty(value = "A list of options ")
  @JsonProperty("options")
  public List<String> getOptions() {
    return options;
  }

  @JsonProperty("options")
  public void setOptions(List<String> options) {
    this.options = options;
  }

  public TestSchema addOptionsItem(String optionsItem) {
    if (this.options == null) {
      this.options = new ArrayList<>();
    }

    this.options.add(optionsItem);
    return this;
  }

  public TestSchema removeOptionsItem(String optionsItem) {
    if (optionsItem != null && this.options != null) {
      this.options.remove(optionsItem);
    }

    return this;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    TestSchema testSchema = (TestSchema) o;
    return Objects.equals(this.options, testSchema.options);
  }

  @Override
  public int hashCode() {
    return Objects.hash(options);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class TestSchema {\n");
    
    sb.append("    options: ").append(toIndentedString(options)).append("\n");
    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    ");
  }


}

Notably I tried this with the 6.4.0 branch checkout using CLI, but it also failed to generate initializing the options to a new ArrayList<>();, so I'm not sure if this is has always failed on command line generation but worked on Gradle previously.

So to summarize:
CLI on all versions I checked (6.4.0, 6.6.0, master) all give the first response, as does Gradle 6.6.0.

Gradle 6.4.0 gives the second response (correct response)

@VinceGall
Copy link

VinceGall commented Jul 10, 2023

Same issue (Swagger 2.0 specs).
Not required arrays should be initialized to null. Required arrays should be initialized with new ...

@wing328
Copy link
Member

wing328 commented Jul 16, 2023

can you please give it another with 7.0.0-beta released last week?

https://github.com/OpenAPITools/openapi-generator/pull/new/7.0.0-beta

@kinsleyT
Copy link
Author

kinsleyT commented Jul 20, 2023

Hi, thanks for following up. Using the Gradle plugin id 'org.openapi.generator' version '7.0.0-beta' still leads to the same results unfortunately, and containerDefaultToNull: 'false', is still not being respected

@frankjkelly
Copy link

frankjkelly commented Jul 25, 2023

FYI just upgraded to 6.4.0 and seeing a similar thing for the Spring generator (similarly tested with 6.5.0, 6.5.0 and 7.0.0-beta and using containerDefaultToNull: true. Should I file a separate bug?
This was working for me in 5.3.1.

I had

 private Map<String, String> tags = null;

now I get

  private Map<String, String> tags = new HashMap<>();

@wing328
Copy link
Member

wing328 commented Jul 26, 2023

Another way is to add nullalble: true to the schema if you want the map to initialise with null.

The containerDefaultToNull option should work though (as a fallback). Can you please file a separate issue with a minimal spec to reproduce the issue?

@frankjkelly
Copy link

frankjkelly commented Jul 26, 2023

Thanks @wing328 that helped a little bit - the model definition now shows

  private JsonNullable<Map<String, String>> tags = JsonNullable.<Map<String, String>>undefined();

however the associated DTO objects don't seem to be aware of this change - I see the following

@JsonTypeName("signal-type")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2023-07-26T09:25:31.425703-04:00[America/New_York]")
public class SignalType {
.
.
  @Valid
  private JsonNullable<Map<String, String>> tags = JsonNullable.undefined();
.
.
  public SignalType putTagsItem(String key, String tagsItem) {
    if (this.tags == null) {
      this.tags = new HashMap<>(); <--- This does not compile
    }
    this.tags.put(key, tagsItem); <-- Neither does this
    return this;
  }

Am I missing something?

@wing328
Copy link
Member

wing328 commented Jul 26, 2023

Can you please share a spec to reproduce the issue?

@frankjkelly
Copy link

@wing328 Sorry for the delay - will try to code up a spec to help repro today.

@frankjkelly
Copy link

frankjkelly commented Aug 1, 2023

@wing328 For my issue above, all my problems went away when

  1. Upgraded to 7.0.0-beta gradle plugin
  2. Set containerDefaultToNull : true for client code
  3. Left containerDefaultToNull to the default(false) for server-side stubs.
    Thanks for your help and advice and assistance.

@wing328
Copy link
Member

wing328 commented Aug 2, 2023

@frankjkelly thanks for confirming the fix.

@twbecker
Copy link

I'm seeing this also after moving from 6.3.0 to 7.0.1 but given #14875 this seems like the intended behavior. But the containerDefaultToNull option seems broken. Based on #14130 it is supposed to allow fallback to the previous behavior but it doesn't. It only allows you to opt into null defaults, not out of them.

@mimkorn
Copy link

mimkorn commented Nov 23, 2023

We actually get an empty list always and can't get it to null.

We have a query parameter defined like

        - in: query
          name: slices
          required: false
          schema:
            type: array
            nullable: true
            items:
              type: string

It is set to be a non-required nullable array and config option containerDefaultToNull was set to true (tried false as well)
and it always generates empty array.

There's this in ApiClient generated:

public List<Pair> parameterToPairs(String collectionFormat, String name, Object value){
    List<Pair> params = new ArrayList<Pair>();

    // preconditions
    if (name == null || name.isEmpty() || value == null) return params;

as you can see, when value is null, empty arrayList is returned. This is in 6.6.0 and 7.1.0

@jorgerod
Copy link
Contributor

jorgerod commented Mar 8, 2024

Hi

Any news? Is there a workaround?

@frankjkelly
Copy link

Hi

Any news? Is there a workaround?

This worked for me #15891 (comment)

@MelleD
Copy link
Contributor

MelleD commented Mar 11, 2024

@wing328 is there a reason why optional and required handles differently here:

https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java#L1182

I would expect for both a empty list/set when containerDefaultToNull is false

IMHO we should change this to, because the required field is not important with the containerDefaultToNull option

 if (cp.isNullable || containerDefaultToNull) {
          return null;
        }

@wing328
Copy link
Member

wing328 commented Mar 11, 2024

can you please submit a PR with the suggested fix?

@MelleD
Copy link
Contributor

MelleD commented Mar 11, 2024

can you please submit a PR with the suggested fix?

Yes I will do

@MelleD
Copy link
Contributor

MelleD commented Mar 12, 2024

See here: #18080

@wing328 wing328 closed this as completed Mar 18, 2024
@wing328
Copy link
Member

wing328 commented Mar 18, 2024

please give it a try with the latest snapshot version: https://oss.sonatype.org/content/repositories/snapshots/org/openapitools/openapi-generator-cli/7.5.0-SNAPSHOT/

you may need a normalizer rule to rollback to previous behavior: #18128

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants