OpenAPITools / OpenAPITools/openapi-generator

[BUG][Java][Spring][SpringBoot] optional array with 'minItems' set, fails validation

Open
#22,784 15 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?
  • 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

given an optioal array with 'minItems: 1', fails validation if this array isn't provided by the client.

All arrays defined in OpenAPI are, by default, initialized as empty lists in Java. If an array is required to have at least one entry when it is provided, validation will fail if the client omits the array.

openapi-generator version

v7.18.0, v7.19.0, master

OpenAPI declaration file content or url

OpenApi declaration:

openapi: 3.1.1
info:
    title: Optional Array OpenAPI Example
    version: 1.0.0
paths:
  /v1/new-customer:
    put:
      operationId: setNewCustomer
      requestBody:
        description: the new customer to be created
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewCustomerRequest'
      responses:
        204:
          description: The new customer was set successfully.

components:
  schemas:
    NewCustomerRequest:
      type: object
      properties:
        newCustomer:
          properties:
            name:
              type: string
          required:
            - name
        documents:
          description: |
            the documents to be attached to identify the new customer.
            This array is optional. If provided, it must contain between 1 and 3 items
          type: array
          items:
            $ref: '#/components/schemas/Document'
          minItems: 1
          maxItems: 3
      required:
        - newCustomer
    Document:
      type: object
      properties:
        documentId:
          description: |
            The document id (uuid)
          type: string
          format: uuid
        documentType:
          $ref: '#/components/schemas/DocumentType'
        documentNo:
          type: string
      required:
        - documentId
        - documentType
        - documentNo
    DocumentType:
      type: string
      enum:
        - ID_CARD
        - PASSPORT
Generation Details

java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate
-i optional_array-openapi.yaml
-g spring
-o tmp/spring-api

generates this model:

package org.openapitools.model;

import java.net.URI;
import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.openapitools.model.Document;
import org.openapitools.model.NewCustomerRequestNewCustomer;
import org.springframework.lang.Nullable;
import org.openapitools.jackson.nullable.JsonNullable;
import java.time.OffsetDateTime;
import javax.validation.Valid;
import javax.validation.constraints.*;
import io.swagger.v3.oas.annotations.media.Schema;


import java.util.*;
import javax.annotation.Generated;

/**
 * NewCustomerRequest
 */

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2026-01-23T09:37:18.269659+01:00[Europe/Zurich]", comments = "Generator version: 7.20.0-SNAPSHOT")
public class NewCustomerRequest {

  private NewCustomerRequestNewCustomer newCustomer;

  @Valid
  private List<@Valid Document> documents = new ArrayList<>();

  public NewCustomerRequest() {
    super();
  }

  /**
   * Constructor with only required parameters
   */
  public NewCustomerRequest(NewCustomerRequestNewCustomer newCustomer) {
    this.newCustomer = newCustomer;
  }

  public NewCustomerRequest newCustomer(NewCustomerRequestNewCustomer newCustomer) {
    this.newCustomer = newCustomer;
    return this;
  }

  /**
   * Get newCustomer
   * @return newCustomer
   */
  @NotNull @Valid 
  @Schema(name = "newCustomer", requiredMode = Schema.RequiredMode.REQUIRED)
  @JsonProperty("newCustomer")
  public NewCustomerRequestNewCustomer getNewCustomer() {
    return newCustomer;
  }

  public void setNewCustomer(NewCustomerRequestNewCustomer newCustomer) {
    this.newCustomer = newCustomer;
  }

  public NewCustomerRequest documents(List<@Valid Document> documents) {
    this.documents = documents;
    return this;
  }

  public NewCustomerRequest addDocumentsItem(Document documentsItem) {
    if (this.documents == null) {
      this.documents = new ArrayList<>();
    }
    this.documents.add(documentsItem);
    return this;
  }

  /**
   * the documents to be attached to identify the new customer. This array is optional. If provided, it must contain between 1 and 3 items 
   * @return documents
   */
  @Valid @Size(min = 1, max = 3) 
  @Schema(name = "documents", description = "the documents to be attached to identify the new customer. This array is optional. If provided, it must contain between 1 and 3 items ", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
  @JsonProperty("documents")
  public List<@Valid Document> getDocuments() {
    return documents;
  }

  public void setDocuments(List<@Valid Document> documents) {
    this.documents = documents;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    NewCustomerRequest newCustomerRequest = (NewCustomerRequest) o;
    return Objects.equals(this.newCustomer, newCustomerRequest.newCustomer) &&
        Objects.equals(this.documents, newCustomerRequest.documents);
  }

  @Override
  public int hashCode() {
    return Objects.hash(newCustomer, documents);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class NewCustomerRequest {\n");
    sb.append("    newCustomer: ").append(toIndentedString(newCustomer)).append("\n");
    sb.append("    documents: ").append(toIndentedString(documents)).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(@Nullable Object o) {
    if (o == null) {
      return "null";
    }
    return o.toString().replace("\n", "\n    ");
  }
}
Steps to reproduce

given the declaration above, generate the java code and execute this test:

package org.openapitools.model;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;

public class NewCustomerRequestTest {

	private final Validator validator;

	public NewCustomerRequestTest() {
		try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
			this.validator = factory.getValidator();
		}
	}

    @Test
    void testNewCustomerReadFromJsonAndValidate()
            throws Exception {
        final String newCustomerJsonString = "{\"newCustomer\":{\"name\":\"test customer name\"}}";
        final NewCustomerRequest newCustomer = new ObjectMapper().readValue(newCustomerJsonString, NewCustomerRequest.class);
        final Set<ConstraintViolation<NewCustomerRequest>> newCustomerValidations = validator.validate(newCustomer);
        assertThat(newCustomerValidations).isEmpty();
    }

}
Related issues/PRs

https://github.com/OpenAPITools/openapi-generator/issues/18735
https://github.com/OpenAPITools/openapi-generator/pull/21269

Suggest a fix

The generator must check whether an array has 'minItems' defined and whether it is optional. If so, the array must not be initialized by default in Java.

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 documented generation command with optional_array-openapi.yaml and inspect the generated NewCustomerRequest model, especially its optional documents field. Use NewCustomerRequestTest as the reproduction; done means omitting documents produces no validation violations while minItems still applies when the array is provided.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, spring, spring-boot
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.