Skip to content

Comments

fixes(spring) add JsonProperty on setter#22978

Open
antechrestos wants to merge 1 commit intoOpenAPITools:masterfrom
antechrestos:fixes/22757-add-jackson-property-on-setter
Open

fixes(spring) add JsonProperty on setter#22978
antechrestos wants to merge 1 commit intoOpenAPITools:masterfrom
antechrestos:fixes/22757-add-jackson-property-on-setter

Conversation

@antechrestos
Copy link
Contributor

@antechrestos antechrestos commented Feb 15, 2026

In order to fix #22757, I applied same logic used on java side by #9041

  • extract jacksonAnnotation partial template
  • extract xmlAccessorAnnotation partial template
  • apply jacksonAnnotation and xmlAccessorAnnotation partial templates on both getter and setter

Summary by cubic

Add @JsonProperty to generated Java Spring model setters so Jackson maps JSON names correctly during deserialization. Apply the same annotation block to getters and XML accessors, excluding properties marked as jackson-optional-nullable (fixes #22757).

  • Bug Fixes

    • Add @JsonProperty on setters when using Jackson, excluding jackson-optional-nullable properties.
    • Regenerate Spring samples to annotate setters (covers names like "@type", "123-list", dashes/spaces).
  • Refactors

    • Extract jackson_annotations and xmlAccessorAnnotation partials; reuse in getters/setters to deduplicate templates.
    • Update JDK17 samples workflow to watch nested directories and fix spring-boot-oneof-sealed path.

Written for commit 942ec96. Summary will update on new commits.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1178 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

@antechrestos antechrestos force-pushed the fixes/22757-add-jackson-property-on-setter branch from 81ab5b7 to e397869 Compare February 15, 2026 13:41
@antechrestos
Copy link
Contributor Author

@cubic-dev-ai rerun a review

@cubic-dev-ai
Copy link
Contributor

cubic-dev-ai bot commented Feb 15, 2026

@cubic-dev-ai rerun a review

@antechrestos I have started the AI code review. It will take a few minutes to complete.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1335 files

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

@antechrestos
Copy link
Contributor Author

antechrestos commented Feb 15, 2026

@wing328 here's the fix I was waiting to push after the merge of #22854

As suggested in the issue #22757 , I applied the fix that already is present on java side; also, I used partial so as not to repeat myself.

There are tons of modified files yet difference consist in thee

$ git diff HEAD~1  --name-only  | grep -v samples/
modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
new file mode 100644
index 00000000000..0668f40785c
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
@@ -0,0 +1,7 @@
+  @JsonProperty("{{baseName}}")
+{{#withXml}}
+  @JacksonXmlProperty(localName = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#isXmlAttribute}}, isAttribute = true{{/isXmlAttribute}}{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+  {{#isContainer}}
+  @JacksonXmlElementWrapper({{#isXmlWrapped}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}", {{#xmlNamespace}}namespace = "{{.}}", {{/xmlNamespace}}{{/isXmlWrapped}}useWrapping = {{isXmlWrapped}})
+  {{/isContainer}}
+{{/withXml}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
index 82a0a6eb00c..8a93fb054f1 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
@@ -226,25 +226,10 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
   {{#swagger1AnnotationLibrary}}
   @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")
   {{/swagger1AnnotationLibrary}}
-  {{#jackson}}
-  @JsonProperty("{{baseName}}")
-  {{#withXml}}
-  @JacksonXmlProperty(localName = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#isXmlAttribute}}, isAttribute = true{{/isXmlAttribute}}{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{#isContainer}}
-  @JacksonXmlElementWrapper({{#isXmlWrapped}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}", {{#xmlNamespace}}namespace = "{{.}}", {{/xmlNamespace}}{{/isXmlWrapped}}useWrapping = {{isXmlWrapped}})
-    {{/isContainer}}
-  {{/withXml}}
-  {{/jackson}}
-  {{#withXml}}
-  @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{#isXmlWrapped}}
-  @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{/isXmlWrapped}}
-  {{/withXml}}
   {{#deprecated}}
   @Deprecated
   {{/deprecated}}
-  public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
+{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}}  public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
     return {{name}};
   }
   {{/lombok.Getter}}
@@ -261,7 +246,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
   {{#deprecated}}
   @Deprecated
   {{/deprecated}}
-  public void {{setter}}({{>nullableAnnotation}}{{>nullableDataType}} {{name}}) {
+{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{>jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}}  public void {{setter}}({{>nullableAnnotation}}{{>nullableDataType}} {{name}}) {
     this.{{name}} = {{name}};
   }
   {{/lombok.Setter}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache
new file mode 100644
index 00000000000..ee5195c0eb5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache
@@ -0,0 +1,4 @@
+  @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+{{#isXmlWrapped}}
+  @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+{{/isXmlWrapped}}

@wing328
Copy link
Member

wing328 commented Feb 15, 2026

Thanks for the PR

cc @cachescrubber (2022/02) @welshm (2022/02) @MelleD (2022/02) @atextor (2022/02) @manedev79 (2022/02) @javisst (2022/02) @borsch (2022/02) @banlevente (2022/02) @Zomzog (2022/09) @martin-mfg (2023/08)

@antechrestos antechrestos force-pushed the fixes/22757-add-jackson-property-on-setter branch 2 times, most recently from 21562c7 to c00186a Compare February 16, 2026 08:14
@antechrestos
Copy link
Contributor Author

antechrestos commented Feb 16, 2026

@wing328 I was wondering why previous run of Sample Java Spring were not launched. When taking a look at the action page, I saw that it was only launched on release 😨

To cap it all, one of the path was wrong.

To fix it, I made the following fixes and now job are launched.

@antechrestos
Copy link
Contributor Author

@jpfinne
Copy link
Contributor

jpfinne commented Feb 19, 2026

@antechrestos I've found another issue with jackson 3.

By default, a constructor with argument is used to deserialize.

So if there is a required arguments or an all arguments constructor, it will take precedence.

For example:

public class SomeObj {
  public SomeObj() {
  }
  public SomeObj(UUID xRequestId)) {
     this.xRequestId = xRequestId;
  }

  @JsonProperty("xRequestId")
  public void setxRequestId(UUID xRequestId) {
    this.xRequestId = xRequestId;
  }

Jackson uses the constructor and fails to initializer xRequestId because there is no @JsonProperty on the argument.

The issue is very bad for lists:

  public class TestData {
      private List<String> list = new ArrayList<>();

      public TestData() {
      }
      public TestData(List<String> list) {
          this.list = list;
      }
      
      @JsonProperty("list")
      public void setList(List<String> list) {
          this.list = list;
      }
  }

Deserializing { } gives an empty list with jackson2 and a null list with jackson3

It is possible to configure the JsonMapper with constructorDetector.EXPLICIT_ONLY
But I've not managed to use it with the documented property spring.jackson.constructor-detector: explicit_only

A simpler and more robust solution is to annotated the constructor with @JsonIgnore
Example:

@JsonIgnore
public TestData(List<String> list) {
    this.list = list;
}

Can you add the annotation at line 105 and 128 in pojo.mustache?

It won't work for lombok generated constructor though.

@antechrestos
Copy link
Contributor Author

@jpfinne To my point of view, I think that the setter approach is quite a bad pattern.

I would better

  • remove setter
  • have a all arguments constructor
  • no no arg constructor

This would provide immutability which is not a bad approach.

As an alternative so as have not that much breaking change, I would propose something like

public class TestData {

      private String someStringAttribute;
      private List<String> list = new ArrayList<>();

     // so as not to break code
      public TestData() {
      }


      @JsonCreator // this will force jackson to use this, good semantic, this will host deserialisation declaration
      public TestData(
         // here for deserialization
         @JsonProperty("someStringAttribute") String someStringAttribute, 
         // here for deserialization
         @JsonProperty("list")          List<String> list
       ) {
          this.someStringAttribute = someStringAttribute;
          this.list = list;
      }

     // here for serialization
      @JsonProperty("someStringAttribute") 
      public String getSomeStringAttribute() {
          return this.someStringAttribute;
      }

     // here for serialization
      @JsonProperty("list")
      public void getList() {
          return this.list ;
      }

     // just for coder using noarg constructor
      public void setSomeStringAttribute(String someStringAttribute) {
          this.someStringAttribute= someStringAttribute;
      }

      // just for coder using noarg constructor
      public void setList(List<String> list) {
          this.list = list;
      }
  }

This approach is the one that enforce requirement when you set required: true on JsonProperty on a constructor annotated JsonCreator.

What do you think of it?

@jpfinne
Copy link
Contributor

jpfinne commented Feb 20, 2026

@antechrestos I've tried this approch.

See https://github.com/jpfinne/openapi-generator/blob/feature/JsonPropertyInAllArgsConstructor/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache

My PR gave hundred of errors from cubic-dev-ai. So I close it.

The reason is that the assignments in the constructor must assign a value equivalent to the default value in the field declaration when the argument is null.

In your example:

@JsonCreator
public TestData( @JsonProperty("someStringAttribute") String someStringAttribute, @JsonProperty("list") List<String> list) {
    this.someStringAttribute = someStringAttribute;
    this.list = list != null: new ArrayList<>();
}

The default values for list depend on multiple factors:

  • required field
  • openApiNullable
  • useOptional
  • optionalAcceptNullable
  • containerDefaultToNull

The constructor generation depends on:

  • required fields
  • generateConstructorWithAllArgs
  • generatedConstructorWithRequiredArgs
  • lombok

Good luck to keep backward compatibility in all java generators that use jackson. Any breaking change triggers an avalanche of criticism.
It would be easier to have a custom opiniated pojo.mustache for jackson3.

So I would go for @JsonProperty on setters and @JsonIgnore on constructor .
It is the easiest to keep backward compatibility and fixes jackson3 deserialization.

@antechrestos
Copy link
Contributor Author

antechrestos commented Feb 20, 2026

@jpfinne that is sad because this is the right way of deserializing in jackson. Moreover this way lets you handle require fields using the required JsonProperty attribute.

As an example the following code illustrate this mechanism

package org.demo.jackson.deserialisation;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;

import java.util.List;
import java.util.Optional;

public class JacksonDeserializationTest {

    public static class Payload {

        private String xRequestId;

        private List<String> xRequestIds;

        @JsonProperty("x-request-id")
        public String getxRequestId() {
            return xRequestId;
        }

        @JsonProperty("x-request-ids")
        public List<String> getxRequestIds() {
            return xRequestIds;
        }

        public Payload() {
        }

        public Payload(String xRequestId) {
            this.xRequestId = xRequestId;
        }

        public Payload(List<String> xRequestIds) {
            this.xRequestIds = xRequestIds;
        }

        @JsonCreator
        public Payload(
                @JsonProperty("x-request-id") String xRequestId,
                @JsonProperty("x-request-ids") List<String> xRequestIds
        ) {
            this.xRequestId = xRequestId;
            this.xRequestIds = xRequestIds;
            JacksonDeserializationTest.byAllArgConstructor = true;
        }
    }


    static boolean byAllArgConstructor = false;

    private static final String JSON = """
            {
                "x-request-id": "the-request-id",
                "x-request-ids": ["the-first-request-id", "the-second-request-id"]
            }
            """;

    private static void checkJackson2() {
        byAllArgConstructor = false;
        try {
            Payload payload = new ObjectMapper().readValue(JSON, Payload.class);
            checkAssertions(payload);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void checkJackson3() {
        byAllArgConstructor = false;
        var payload = JsonMapper.builder().build().readValue(JSON, JacksonDeserializationTest.Payload.class);
        checkAssertions(payload);
    }

    private static void checkAssertions(Payload payload) {
        assert "the-request-id".equals(payload.xRequestId);
        assert 2 == Optional.ofNullable(payload.xRequestIds).map(List::size).orElse(0);
        assert "the-first-request-id".equals(payload.xRequestIds.get(0));
        assert "the-second-request-id".equals(payload.xRequestIds.get(1));
        assert byAllArgConstructor;
    }

    public static void main(String[] args) {
        System.out.println("Testing jackson 2...");
        checkJackson2();
        System.out.println("Jackson 2 OK");
        System.out.println("Testing jackson 3...");
        checkJackson3();
        System.out.println("Jackson 3 OK");
    }
}

@jpfinne
Copy link
Contributor

jpfinne commented Feb 20, 2026

backward and forward compatibility is more important

* extract jacksonAnnotation partial template
* extract xmkAccessorAnnotation partial template
* apply jacksonAnnotation partial template on both getter and setter

Fixes OpenAPITools#22757
@antechrestos antechrestos force-pushed the fixes/22757-add-jackson-property-on-setter branch from c00186a to 942ec96 Compare February 20, 2026 19:50
@antechrestos
Copy link
Contributor Author

@jpfinne

what I don't understand is that with jackson3 and jackson 2, without any JsonIgnore everything works well, I must miss something

public class JacksonDeserializationTest {

    public static class Payload {

        private UUID xRequestId;

        private List<UUID> xRequestIds;

        private List<String> unknwonProperty = new ArrayList<>();

        @JsonProperty("x-request-id")
        public UUID getxRequestId() {
            return xRequestId;
        }

        @JsonProperty("x-request-ids")
        public List<UUID> getxRequestIds() {
            return xRequestIds;
        }

        @JsonProperty("unknown-property")
        public List<String> getUnknwonProperty() {
            return unknwonProperty;
        }

        @JsonProperty("x-request-id")
        public void setxRequestId(UUID xRequestId) {
            this.xRequestId = xRequestId;
        }

        @JsonProperty("x-request-ids")
        public void setxRequestIds(List<UUID> xRequestIds) {
            this.xRequestIds = xRequestIds;
        }

        @JsonProperty("unknown-property")
        public void setUnknwonProperty(List<String> unknwonProperty) {
            this.unknwonProperty = unknwonProperty;
        }

        public Payload() {
        }

        public Payload(UUID xRequestId) {
            this.xRequestId = xRequestId;
        }

        public Payload(List<UUID> xRequestIds) {
            this.xRequestIds = xRequestIds;
        }

        public Payload(
                UUID xRequestId,
                List<UUID> xRequestIds,
                List<String> unknwonProperty
        ) {
            this.xRequestId = xRequestId;
            this.xRequestIds = xRequestIds;
            this.unknwonProperty = unknwonProperty;
        }
    }


    private static final String JSON = """
            {
                "x-request-id": "9f09113b-1f95-4c8a-b90a-c5bf6c35c4d9",
                "x-request-ids": ["9f09113b-1f95-4c8a-b90a-c5bf6c35c4da", "9f09113b-1f95-4c8a-b90a-c5bf6c35c4db"]
            }
            """;

    private static void checkJackson2() {
        try {
            Payload payload = new ObjectMapper().readValue(JSON, Payload.class);
            checkAssertions(payload);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private static void checkJackson3() {
        var payload = JsonMapper.builder().build().readValue(JSON, JacksonDeserializationTest.Payload.class);
        checkAssertions(payload);
    }

    private static void checkAssertions(Payload payload) {
        assert "the-request-id".equals(payload.xRequestId);
        assert 2 == Optional.ofNullable(payload.xRequestIds).map(List::size).orElse(0);
        assert "the-first-request-id".equals(payload.xRequestIds.get(0));
        assert "the-second-request-id".equals(payload.xRequestIds.get(1));
        assert payload.unknwonProperty != null;
        assert payload.unknwonProperty.size() == 1;
    }

    public static void main(String[] args) {
        try{
            System.out.println("Testing jackson 2...");
            checkJackson2();
            System.out.println("Jackson 2 OK");
        } catch (Throwable t){
            System.err.println("Jackson 2 KO");
            t.printStackTrace(System.err);
        }

        try {
            System.out.println("Testing jackson 3...");
            checkJackson3();
            System.out.println("Jackson 3 OK");
        } catch (Throwable t){
            System.err.println("Jackson 3 KO");
            t.printStackTrace(System.err);
        }
    }
}

So I think going with the annotated setters as on the java generator will do the job... mostly.

For list initiated in the field resulting in null while deserializing a payload without any entry of the field, I would expect it to be the normal behaviour (despite it is not in my example) as the payload does not have the field and the object should reflect it by a null value (as we do not have a undefined value in java).

As you mentionned, refactoring the constructor handling lombok annotations would be tricky and error prone, while adding the annotation on setter is mature thanks to the java generator which already does it.

I repush my change with a mutualisation of of jackson annotation which were in the jacksonAnnotation partial mustache file.

@antechrestos
Copy link
Contributor Author

Modified files

.github/workflows/samples-spring-jdk17.yaml
modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache
modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache

And the differences

diff --git a/.github/workflows/samples-spring-jdk17.yaml b/.github/workflows/samples-spring-jdk17.yaml
index 9e48ff2f74d..daf09a63412 100644
--- a/.github/workflows/samples-spring-jdk17.yaml
+++ b/.github/workflows/samples-spring-jdk17.yaml
@@ -3,32 +3,32 @@ name: Samples Java Spring (JDK17)
 on:
   push:
     paths:
-      - samples/openapi3/client/petstore/spring-cloud-3-with-optional
-      - samples/openapi3/client/petstore/spring-cloud-4-with-optional
-      - samples/client/petstore/spring-http-interface-springboot-4
-      - samples/openapi3/server/petstore/springboot-3
-      - samples/openapi3/server/petstore/springboot-4
-      - samples/server/petstore/springboot-api-response-examples
-      - samples/server/petstore/springboot-lombok-data
-      - samples/server/petstore/springboot-lombok-tostring
-      - samples/server/petstore/springboot-file-delegate-optional
-      - samples/server/petstore/springboot-petstore-with-api-response-examples
-      - samples/server/petstore/spring-boot-oneof-sealed
-      - samples/openapi3/server/petstore/spring-boot-oneof-interface
+      - samples/openapi3/client/petstore/spring-cloud-3-with-optional/**
+      - samples/openapi3/client/petstore/spring-cloud-4-with-optional/**
+      - samples/client/petstore/spring-http-interface-springboot-4/**
+      - samples/openapi3/server/petstore/springboot-3/**
+      - samples/openapi3/server/petstore/springboot-4/**
+      - samples/server/petstore/springboot-api-response-examples/**
+      - samples/server/petstore/springboot-lombok-data/**
+      - samples/server/petstore/springboot-lombok-tostring/**
+      - samples/server/petstore/springboot-file-delegate-optional/**
+      - samples/server/petstore/springboot-petstore-with-api-response-examples/**
+      - samples/openapi3/server/petstore/spring-boot-oneof-sealed/
+      - samples/openapi3/server/petstore/spring-boot-oneof-interface/**
   pull_request:
     paths:
-      - samples/openapi3/client/petstore/spring-cloud-3-with-optional
-      - samples/openapi3/client/petstore/spring-cloud-4-with-optional
-      - samples/client/petstore/spring-http-interface-springboot-4
-      - samples/openapi3/server/petstore/springboot-3
-      - samples/openapi3/server/petstore/springboot-4
-      - samples/server/petstore/springboot-api-response-examples
-      - samples/server/petstore/springboot-lombok-data
-      - samples/server/petstore/springboot-lombok-tostring
-      - samples/server/petstore/springboot-file-delegate-optional
-      - samples/server/petstore/springboot-petstore-with-api-response-examples
-      - samples/server/petstore/spring-boot-oneof-sealed
-      - samples/openapi3/server/petstore/spring-boot-oneof-interface
+      - samples/openapi3/client/petstore/spring-cloud-3-with-optional/**
+      - samples/openapi3/client/petstore/spring-cloud-4-with-optional/**
+      - samples/client/petstore/spring-http-interface-springboot-4/**
+      - samples/openapi3/server/petstore/springboot-3/**
+      - samples/openapi3/server/petstore/springboot-4/**
+      - samples/server/petstore/springboot-api-response-examples/**
+      - samples/server/petstore/springboot-lombok-data/**
+      - samples/server/petstore/springboot-lombok-tostring/**
+      - samples/server/petstore/springboot-file-delegate-optional/**
+      - samples/server/petstore/springboot-petstore-with-api-response-examples/**
+      - samples/openapi3/server/petstore/spring-boot-oneof-sealed/**
+      - samples/openapi3/server/petstore/spring-boot-oneof-interface/**
 jobs:
   build:
     name: Build Java Spring (JDK17)
@@ -49,7 +49,7 @@ jobs:
           - samples/server/petstore/springboot-lombok-tostring
           - samples/server/petstore/springboot-file-delegate-optional
           - samples/server/petstore/springboot-petstore-with-api-response-examples
-          - samples/server/petstore/spring-boot-oneof-sealed
+          - samples/openapi3/server/petstore/spring-boot-oneof-sealed
           - samples/openapi3/server/petstore/spring-boot-oneof-interface
     steps:
       - uses: actions/checkout@v5
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
new file mode 100644
index 00000000000..0668f40785c
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/jackson_annotations.mustache
@@ -0,0 +1,7 @@
+  @JsonProperty("{{baseName}}")
+{{#withXml}}
+  @JacksonXmlProperty(localName = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#isXmlAttribute}}, isAttribute = true{{/isXmlAttribute}}{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+  {{#isContainer}}
+  @JacksonXmlElementWrapper({{#isXmlWrapped}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}", {{#xmlNamespace}}namespace = "{{.}}", {{/xmlNamespace}}{{/isXmlWrapped}}useWrapping = {{isXmlWrapped}})
+  {{/isContainer}}
+{{/withXml}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache
index 50834e20238..6b09b602fe2 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/lombokAnnotation.mustache
@@ -23,14 +23,7 @@
   {{#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}}
-  {{#jackson}}@JsonProperty("{{baseName}}")
-    {{#withXml}}
-      @JacksonXmlProperty(localName = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#isXmlAttribute}}, isAttribute = true{{/isXmlAttribute}}{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-      {{#isContainer}}
-        @JacksonXmlElementWrapper({{#isXmlWrapped}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}", {{#xmlNamespace}}namespace = "{{.}}", {{/xmlNamespace}}{{/isXmlWrapped}}useWrapping = {{isXmlWrapped}})
-      {{/isContainer}}
-    {{/withXml}}
-  {{/jackson}}
+{{#jackson}}{{>jackson_annotations}}{{/jackson}}
 {{/lombok.Data}}
 {{#lombok.Builder}}
   {{#defaultValue}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
index 82a0a6eb00c..8a93fb054f1 100644
--- a/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/pojo.mustache
@@ -226,25 +226,10 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
   {{#swagger1AnnotationLibrary}}
   @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}")
   {{/swagger1AnnotationLibrary}}
-  {{#jackson}}
-  @JsonProperty("{{baseName}}")
-  {{#withXml}}
-  @JacksonXmlProperty(localName = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#isXmlAttribute}}, isAttribute = true{{/isXmlAttribute}}{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{#isContainer}}
-  @JacksonXmlElementWrapper({{#isXmlWrapped}}localName = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}", {{#xmlNamespace}}namespace = "{{.}}", {{/xmlNamespace}}{{/isXmlWrapped}}useWrapping = {{isXmlWrapped}})
-    {{/isContainer}}
-  {{/withXml}}
-  {{/jackson}}
-  {{#withXml}}
-  @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{#isXmlWrapped}}
-  @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
-    {{/isXmlWrapped}}
-  {{/withXml}}
   {{#deprecated}}
   @Deprecated
   {{/deprecated}}
-  public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
+{{#jackson}}{{>jackson_annotations}}{{/jackson}}{{#withXml}}{{>xmlAccessorAnnotation}}{{/withXml}}  public {{>nullableAnnotation}}{{>nullableDataTypeBeanValidation}} {{getter}}() {
     return {{name}};
   }
   {{/lombok.Getter}}
@@ -261,7 +246,7 @@ public {{>sealed}}class {{classname}}{{#parent}} extends {{{parent}}}{{/parent}}
   {{#deprecated}}
   @Deprecated
   {{/deprecated}}
-  public void {{setter}}({{>nullableAnnotation}}{{>nullableDataType}} {{name}}) {
+{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{>jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}}  public void {{setter}}({{>nullableAnnotation}}{{>nullableDataType}} {{name}}) {
     this.{{name}} = {{name}};
   }
   {{/lombok.Setter}}
diff --git a/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache b/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache
new file mode 100644
index 00000000000..ee5195c0eb5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/JavaSpring/xmlAccessorAnnotation.mustache
@@ -0,0 +1,4 @@
+  @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+{{#isXmlWrapped}}
+  @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}})
+{{/isXmlWrapped}}

@jpfinne
Copy link
Contributor

jpfinne commented Feb 20, 2026

@antechrestos your test is not realistic. It has 4 constructors. I guess jackson3 does not know which constructor to use and use the setters.
The Spring generator always creates a no arg empty constructor, and optionally a required arg constructor and/or a all arg constructor. So maximum 3 constructors. In my sample I have an all arg constructor and Jacskon3 does use it.

About your sentence "For list initiated in the field resulting in null while deserializing a payload without any entry of the field, I would expect it to be the normal behaviour". This is the behaviour when using containerDefaultToNull=true. Being normal or not is an opinion.

The default is false, so the deserialization by the constructor is a breaking change for lists.

So please add @jsonIgnore before the constructors.
And use containerDefaultToNull=true in your generations to obtain the behaviour you desire.

@antechrestos
Copy link
Contributor Author

@jpfinne the problem is that even the no arg constructor has a condition around it. I am not sure that putting an annotation around all constructor having a parameter will be OK for everyone. Same for the option.

Moreover without real example.

Putting the annotation on setter is a real pain that already was present in jackson 2: in the example I provided, that is jackson 2 which fails. The fix is almost without risk as it should have been ported in spring generator when it was put in Java generator. The person in the issue pointed that this fix resolves the issue.

If there are other issues, I'd rather work on it on a dedicated pr, after having worked on input that illustrates the issue.

That is provided that I get any feedback by a maintainer 😏

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] [Java/Spring/Spring-Boot] @JsonProperty is missing on setters / deserialization problems using SpringBoot4 with field xRequestId

3 participants