OpenAPITools / OpenAPITools/openapi-generator

[BUG][MULTIPLE] 3.1.0 spec, additionalProperties: false with nullable properties generates invalid models

Open
#20,213 9 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue: Bug
Dominant language
Java
Stars
26.8k
Forks
7.7k
PR merge metrics
PR metrics pending

Description

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 our OpenAPI spec using the 3.1.0 specification, we make heavy use of the additionalProperties: false keyword on most of our objects to make validation stricter, both while validating our examples and in the generated server code. By adding they keyword, we ensure we don't get false negatives while validating examples with non-required properties with a typo in the key, and make issues in usage of the APIs clearer.

However, when this keyword is combined with an object with nullable properties, the generated models are invalid (see the example below). I've tested both the Typescript-Angular client generator, the Java OKHTTP client generator and the Java spring-boot API spec generator, and each of them shows the same behaviour: the nullable property is inlined to a new, invalid object resulting in broken code.

openapi-generator version

Tested with version 7.9.0 and 7.10.0, both show the same behaviour.

OpenAPI declaration file content or url
# filename: nullable-properties-with-additional-properties-false.yaml
openapi: 3.1.0
info:
  title: ""
  version: 1.0.0
components:
  schemas:
    # For reference, an object without additionalProperties: false, but with nullable properties
    SampleObject:
      properties:
        someString:
          anyOf:
            - type: string
            - type: 'null'

    # For reference, an object with additionalProperties: false, but without any nullable properties
    ReferenceObject:
      additionalProperties: false
      properties:
        someString:
          type: string

    # The broken case: an object with additionalProperties: false and nullable properties
    SampleObjectWithAdditionalFalse:
      additionalProperties: false
      properties:
        someString:
          anyOf:
            - type: string
            - type: 'null'
Generation Details

Verified with the following configs, and no further settings. Run with java -jar openapi-generator-cli-7.10.0.jar generate --config <config-file-as-shown-in-examples>.yaml and no further customization. I will add additional samples in comments for other generators.

Spring Generator

generatorName: spring
outputDir: ./spring-boot
inputSpec: nullable-properties-with-additional-properties-false.yaml

Generated code below. Note the reference objects to show the expected behaviour:

  • For the simplest reference object, a String field with a NOT_REQUIRED annotation.
  • For the object
// Example objects leave out their imports, the equals(), hashCode(), toString() and toIntendedString() methods for
some brevity.

/**
 * ReferenceObject
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:17:54.855204+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class ReferenceObject {

  private String someString;

  public ReferenceObject someString(String someString) {
    this.someString = someString;
    return this;
  }

  /**
   * Get someString
   * @return someString
   */

  @Schema(name = "someString", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  @JsonProperty("someString")
  public String getSomeString() {
    return someString;
  }

  public void setSomeString(String someString) {
    this.someString = someString;
  }
}

/**
 * ObjectWithAdditionalFalse
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:17:54.855204+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class ObjectWithAdditionalFalse {

  private JsonNullable<String> someString = JsonNullable.<String>undefined();

  public ObjectWithAdditionalFalse someString(String someString) {
    this.someString = JsonNullable.of(someString);
    return this;
  }

  /**
   * Get someString
   * @return someString
   */

  @Schema(name = "someString", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  @JsonProperty("someString")
  public JsonNullable<String> getSomeString() {
    return someString;
  }

  public void setSomeString(JsonNullable<String> someString) {
    this.someString = someString;
  }
}

/**
 * ObjectWithNullableProperties
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:17:54.855204+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class ObjectWithNullableProperties {

  private JsonNullable<String> someString = JsonNullable.<String>undefined();

  public ObjectWithNullableProperties someString(String someString) {
    this.someString = JsonNullable.of(someString);
    return this;
  }

  /**
   * Get someString
   * @return someString
   */

  @Schema(name = "someString", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  @JsonProperty("someString")
  public JsonNullable<String> getSomeString() {
    return someString;
  }

  public void setSomeString(JsonNullable<String> someString) {
    this.someString = someString;
  }
}

/**
 * BrokenObject
 */
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:17:54.855204+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class BrokenObject {

  private BrokenObjectSomeString someString;

  public BrokenObject someString(BrokenObjectSomeString someString) {
    this.someString = someString;
    return this;
  }

  /**
   * Get someString
   * @return someString
   */
  @Valid 
  @Schema(name = "someString", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  @JsonProperty("someString")
  public BrokenObjectSomeString getSomeString() {
    return someString;
  }

  public void setSomeString(BrokenObjectSomeString someString) {
    this.someString = someString;
  }
    /**
    * A container for additional, undeclared properties.
    * This is a holder for any undeclared properties as specified with
    * the 'additionalProperties' keyword in the OAS document.
    */
    private Map<String, Object> additionalProperties;

    /**
    * Set the additional (undeclared) property with the specified name and value.
    * If the property does not already exist, create it otherwise replace it.
    */
    @JsonAnySetter
    public BrokenObject putAdditionalProperty(String key, Object value) {
        if (this.additionalProperties == null) {
            this.additionalProperties = new HashMap<String, Object>();
        }
        this.additionalProperties.put(key, value);
        return this;
    }

    /**
    * Return the additional (undeclared) property.
    */
    @JsonAnyGetter
    public Map<String, Object> getAdditionalProperties() {
        return additionalProperties;
    }

    /**
    * Return the additional (undeclared) property with the specified name.
    */
    public Object getAdditionalProperty(String key) {
        if (this.additionalProperties == null) {
            return null;
        }
        return this.additionalProperties.get(key);
    }

}

/**
 * BrokenObjectSomeString
 */
@JsonTypeName("SampleObjectWithAdditionalFalse_someString")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:17:54.855204+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class BrokenObjectSomeString {
  // This object only has the equals(), hashCode(), toString() and toIntendedString() methods.
}

Note that I've also tried the config below, which seems to make practically no difference - except that the incorrectly introduced wrapper object also gets additionalProperties if set to false:

generatorName: spring
outputDir: ./spring-boot
inputSpec: nullable-properties-with-additional-properties-false.yaml
additionalProperties:
  disallowAdditionalPropertiesIfNotPresent: true

Resulting in the following BrokenObjectSomeString.java:

/**
 * BrokenObjectSomeString
 */

@JsonTypeName("BrokenObject_someString")
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2024-12-01T14:35:50.542378+01:00[Europe/Amsterdam]", comments = "Generator version: 7.10.0")
public class BrokenObjectSomeString {
    /**
    * A container for additional, undeclared properties.
    * This is a holder for any undeclared properties as specified with
    * the 'additionalProperties' keyword in the OAS document.
    */
    private Map<String, Object> additionalProperties;

   // Along with all other fields and methods expected of a model which allows additionalProperties. Note that the underlying type was a String, not an object!
}


Steps to reproduce

Create a file with the contents as above - or just any OpenAPI 3.1.0 spec where an object has both additionalProperties: false and any nullable field, done by adding a oneOf or anyOf where one option is the actual type of the field, and the other is { "type": "null" }.

Run a generate task with any of the mentioned generators (likely others as well).

Related issues/PRs

None that I can find. Can supply one if I find a solution.

Suggest a fix

Researching one, though I already want to have the issue filed for reference.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the openapi-generator-cli generate command against nullable-properties-with-additional-properties-false.yaml for the listed Spring, Java OKHTTP, and Typescript-Angular configurations. Compare the generated models for SampleObject, ReferenceObject, and SampleObjectWithAdditionalFalse; done means nullable fields remain valid scalar properties without an invalid wrapper object or unintended additional-property handling.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, java, spring-boot, typescript
Domain
api, backend-api-design, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.