Jackson causing duplicate field serialization in Spring Boot application

Description:

I have encountered an issue while using Jackson for JSON serialization in my Spring Boot application. Despite having only one field annotated with a custom serializer, the response JSON contains two fields with the same value but different case sensitivity.

Expected Behavior:

The expected JSON response should contain only one field for the pId property:

json
{  
  "pId": "123"  
}

Actual Behavior:

The actual JSON response contains two fields, pId and pid, both with the same value:

json
{  
  "pId": "123",  
  "pid": "123"  
}

Steps to Reproduce:

Define a class MaksData with a single field pId annotated with a custom annotation @DataCode. Create a custom annotation @DataCode that is meta-annotated with @JacksonAnnotationsInside and @JsonSerialize using DataCodeHandler as the serializer. Implement DataCodeHandler as a custom JsonSerializer for handling the serialization of the annotated field. Define a REST controller TestController with a single endpoint /test that returns an instance of MaksData. Start the Spring Boot application and access the /test endpoint. Code Snippets:

(Attach the provided code snippets for MaksData, @DataCode, DataCodeHandler, TestController, and the relevant parts of the pom.xml file.) demo.zip

(domain.java)

public class MaksData {

    @DataCode
    private String pId;

    public String getPId() {
        return pId;
    }

    public void setPId(String pId) {
        this.pId = pId;
    }

}

(DataCode.java)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DataCodeHandler.class)
public @interface DataCode {
}

(DataCodeHandler.java)

public class DataCodeHandler extends JsonSerializer<String> implements ContextualSerializer {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider prov) throws IOException {
        gen.writeString(value);
    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty prop) throws JsonMappingException {
        return prov.findValueSerializer(prop.getType(), prop);
    }
}

(TestController.java)

@RestController
@RequestMapping("/")
public class TestController {
    @GetMapping("/test")
    public MaksData test() {
        MaksData maksData = new MaksData();
        maksData.setPId("123");
        return maksData;
    }
}

(DemoApplication.java)

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

(pom.xml)

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

Environment Details:

Spring Boot Version: 3.2.1 Java Version: 21 Jackson Version: 2.15.3

Comment From: philwebb

I think that you're seeing standard Jackson behavior what isn't related to Spring Boot. When serializing MaksData Jackson is using field serialization for private String pId because of the @JsonSerialize meta-annotation but it's also using method serialization for public String getPId().

If you add @JsonIgnore to the getPId() method the duplicate field goes away.