Skip to content

Implement Jackson3 support and maintain Jackson2 support#117

Open
smals-mavh wants to merge 9 commits intoOpenAPITools:masterfrom
smals-mavh:implement-jackson3-with-jackson2-support
Open

Implement Jackson3 support and maintain Jackson2 support#117
smals-mavh wants to merge 9 commits intoOpenAPITools:masterfrom
smals-mavh:implement-jackson3-with-jackson2-support

Conversation

@smals-mavh
Copy link

@smals-mavh smals-mavh commented Jan 30, 2026

Summary by cubic

Adds Jackson 3 support alongside Jackson 2 so JsonNullable works on both runtimes. Also adds a Java 17 module and updates CI to JDK 17.

  • New Features

    • Jackson 3: JsonNullableJackson3Module with serializers, deserializers, type/value serializer modifiers, and unwrapping writer.
    • Auto-discovery for Jackson 3 via META-INF/services/tools.jackson.databind.JacksonModule.
    • Test coverage for both versions using Jackson2Processor and Jackson3Processor with parameterized tests.
  • Refactors

    • Renamed Jackson 2 classes to JsonNullableJackson2* and wired JsonNullableModule to these implementations.
    • Split BOMs in pom.xml (jackson2-bom and jackson3-bom), added a Java 17 module, and dropped module-info “provides” in favor of service loading due to optional deps.
    • Updated GitHub Actions to JDK 17 and added explicit jackson-databind 2.x to integration tests.
    • Removed deprecated Jackson 3 API usage.

Written for commit 460051d. Summary will update on new commits.

@smals-mavh smals-mavh closed this Jan 30, 2026
@smals-mavh smals-mavh reopened this Jan 30, 2026
Copy link

@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.

1 issue found across 36 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="pom.xml">

<violation number="1" location="pom.xml:40">
P2: Jackson 3 requires Java 17+, but the build is pinned to Java 8. Adding the Jackson 3 BOM/tools.jackson dependencies will make builds fail with class file version incompatibility unless the toolchain is upgraded or Jackson 3 is isolated.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

@smals-mavh
Copy link
Author

Some extra context:

  • Kept the name 'JsonNullableModule' instead of JsonNullableJackson2Module for example to keep some backwards compatibility
  • The jackson library is marked as 'provided' in the pom now (to not have any jackson version conflicts downstream). The disadvantage is that users now need to add jackson to their pom.
    • On that note, we could add jackson-annotations since they're the same for both Jackson2 and Jackson3 for the moment. But might not be super future proof.
  • Parameterized tests are a bit shoe-horned in some places, but to me this seemed to be the 'safest' route to make sure that all tests work with both Jackson versions.
  • Tested in both Spring Boot 3 and Spring Boot 4 projects and had no issues (apart from having to add the jackson libs myself since they're not transitive anymore)

This was referenced Jan 30, 2026
@SirBigoo
Copy link

SirBigoo commented Feb 2, 2026

I'm waiting this PR to be released because I can't migrate to spring 4 without breaking my app behavior... 💯

…upport' into implement-jackson3-with-jackson2-support

# Conflicts:
#	.github/workflows/maven_release.yml
#	.github/workflows/maven_test.yml
@antechrestos
Copy link

@wing328 is there any chance that this PR gets merged? This could be useful to add it once useJackson3 get supported by open api generator

@oneachoice
Copy link

I'm waiting :)

@mikomarrache
Copy link

Is there an ETA for releasing this PR? That will be very helpful for the migration to Spring Boot 4. Thanks!

@wing328
Copy link
Member

wing328 commented Feb 24, 2026

I'll try to get it merged this week and cut a release

@wing328
Copy link
Member

wing328 commented Feb 24, 2026

@mikomarrache
Copy link

I just tested the PR and I found a regression.

The following code doesn't work - name is always undefined even if a name field is present in the JSON. It does work with release 0.2.6.

public static class Pet {
    
    @Size(max = 10)   
    public JsonNullable<String> name = JsonNullable.undefined();
    
    public Pet name(JsonNullable<String> name) {
        this.name = name;
        return this;
    }
}

The following code does work:

public static class Pet {
    
    @Size(max = 10)   
    public JsonNullable<String> name;
    
    public Pet name(JsonNullable<String> name) {
        this.name = name;
        return this;
    }
}

@pvdbosch
Copy link

I updated the PR to fix the integration tests (working together with @smals-mavh). The jackson dependencies aren't transitively added anymore, either jackson 2 or 3 has to be added explicitly. We still have to investigate the regression reported by @mikomarrache .

For projects using a module path, there's an issue: the SPI mechanism with provides can't be used to load the jackson module anymore, because it's become a choice between JsonNullableModule and JsonNullableJackson3Module, and JDK < 25 would make them both required. This is similar as #100 for Jakarta Validation.

We could support SPI from Java 25 onwards using multi-release jar, but I don't see a way to support SPI for Java 17 to 24 without making jackson 2 and 3 a mandatory dependency. Any preference on how to go forward with this or other ideas?

@smals-mavh
Copy link
Author

@mikomarrache, could you tell a bit more about your setup. I was unable to reproduce.
The following testcase worked for me.

package org.openapitools.jackson.nullable;

import com.fasterxml.jackson.core.JsonProcessingException;
import jakarta.validation.constraints.Size;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class DebugTest {

    public static class Pet {

        @Size(max = 10)
        public JsonNullable<String> name = JsonNullable.undefined();

        public Pet name(JsonNullable<String> name) {
            this.name = name;
            return this;
        }
    }

    private static String JSON = "{\"name\":\"testName\"}";

    @Test
    void petTestJackson3() {
        JsonMapper mapper = JsonMapper.builder().addModule(new JsonNullableJackson3Module()).build();
        Pet pet = mapper.readValue(JSON, Pet.class);
        assertTrue(pet.name.isPresent());
        assertEquals("testName", pet.name.get());
        assertEquals(JSON, mapper.writeValueAsString(pet));
    }

    @Test
    void petTestJackson2() throws JsonProcessingException {
        com.fasterxml.jackson.databind.json.JsonMapper mapper = new com.fasterxml.jackson.databind.json.JsonMapper();
        mapper.registerModule(new JsonNullableModule());
        Pet pet = mapper.readValue(JSON, Pet.class);
        assertTrue(pet.name.isPresent());
        assertEquals("testName", pet.name.get());
        assertEquals(JSON, mapper.writeValueAsString(pet));
    }

}

This is also more or less tested here: https://github.com/smals-mavh/jackson-databind-nullable/blob/implement-jackson3-with-jackson2-support/src/test/java/org/openapitools/jackson/nullable/JsonNullableSimpleTest.java#L134

Could you tell us more about the setup / share a reproducer?

@mikomarrache
Copy link

Sorry, my bad, I didn't explain the regression correctly. You can use this test:

package org.openapitools.jackson.nullable;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class RegressionTest {

    public static class Pet {
        final JsonNullable<String> field1 = JsonNullable.undefined();

        public JsonNullable<String> getField1() {
            return field1;
        }
    }

    @Test
    void testJackson() throws com.fasterxml.jackson.core.JsonProcessingException {
        String json = "{\"field1\": \"value\"}";

        Pet petJackson2 = new com.fasterxml.jackson.databind.ObjectMapper()
            .registerModule(new JsonNullableModule()).readValue(json, Pet.class);

        Pet petJackson3 = tools.jackson.databind.json.JsonMapper.builder()
            .addModule(new JsonNullableJackson3Module()).build().readValue(json, Pet.class);

        assertEquals(JsonNullable.of("value"), petJackson2.getField1());
        assertEquals(JsonNullable.of("value"), petJackson3.getField1());
    }
}

@smals-mavh
Copy link
Author

smals-mavh commented Feb 26, 2026

@mikomarrache , good catch!

I don't think this is really a regression in the json-nullable lib, but more a behavioral change in Jackson 3.
See config defaults changes.
Especially this issue
Apparently some changes are on its way on this matter for JDK26

Note that the jackson 2 test you provided still passes, but to let it work on jackson 3 I suppose we have two options.
1.

Pet petJackson3 = tools.jackson.databind.json.JsonMapper.builder()
                .enable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .addModule(new JsonNullableJackson3Module()).build().readValue(json, Pet.class);

But to be seen how future proof this will be.

  1. annotate the class (or use records and it works automatically):
package org.openapitools.jackson.nullable;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.MapperFeature;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

class RegressionTest {

    public static class Pet {
        final JsonNullable<String> field1;

        public Pet() {
            field1 = JsonNullable.undefined();
        }

        public Pet(@JsonProperty("field1") JsonNullable<String> field1) {
            this.field1 = field1;
        }

        public JsonNullable<String> getField1() {
            return field1;
        }
    }

    public record OtherPet(JsonNullable<String> field1){}

    @Test
    void testJackson() throws com.fasterxml.jackson.core.JsonProcessingException {
        String json = "{\"field1\": \"value\"}";
        String undefinedJson = "{}";

        Pet petJackson2 = new com.fasterxml.jackson.databind.ObjectMapper()
                .disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .registerModule(new JsonNullableModule()).readValue(json, Pet.class);

        OtherPet otherPetJackson2 = new com.fasterxml.jackson.databind.ObjectMapper()
                .disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .registerModule(new JsonNullableModule()).readValue(json, OtherPet.class);

        Pet petJackson3 = tools.jackson.databind.json.JsonMapper.builder()
                .addModule(new JsonNullableJackson3Module()).build().readValue(json, Pet.class);

        OtherPet otherPetJackson3 = tools.jackson.databind.json.JsonMapper.builder()
                .addModule(new JsonNullableJackson3Module()).build().readValue(json, OtherPet.class);

        assertEquals(JsonNullable.of("value"), petJackson2.getField1());
        assertEquals(JsonNullable.of("value"), petJackson3.getField1());
        assertEquals(JsonNullable.of("value"), otherPetJackson2.field1());
        assertEquals(JsonNullable.of("value"), otherPetJackson3.field1());

        OtherPet otherPetJackson2Undefined = new com.fasterxml.jackson.databind.ObjectMapper()
                .disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
                .registerModule(new JsonNullableModule()).readValue(undefinedJson, OtherPet.class);

        OtherPet otherPetJackson3Undefined = tools.jackson.databind.json.JsonMapper.builder()
                .addModule(new JsonNullableJackson3Module()).build().readValue(undefinedJson, OtherPet.class);

        assertEquals(JsonNullable.undefined(), otherPetJackson2Undefined.field1());
        assertEquals(JsonNullable.undefined(), otherPetJackson3Undefined.field1());
    }
}

I don't think this is an issue for this library, but might be an annoying behavioral change in Jackson 3.

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.

7 participants