Can't make Jackson and Lombok work together
Asked Answered
C

21

146

I am experimenting in combining Jackson and Lombok. Those are my classes:

package testelombok;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.Wither;

@Value
@Wither
@AllArgsConstructor(onConstructor=@__(@JsonCreator))
public class TestFoo {
    @JsonProperty("xoom")
    private String x;
    private int z;
}
package testelombok;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xebia.jacksonlombok.JacksonLombokAnnotationIntrospector;
import java.io.IOException;

public class TestLombok {

    public static void main(String[] args) throws IOException {
        TestFoo tf = new TestFoo("a", 5);
        System.out.println(tf.withX("b"));
        ObjectMapper om = new ObjectMapper().setAnnotationIntrospector(new JacksonLombokAnnotationIntrospector());
        System.out.println(om.writeValueAsString(tf));
        TestFoo tf2 = om.readValue(om.writeValueAsString(tf), TestFoo.class);
        System.out.println(tf2);
    }

}

Those are the JARs that I'm adding into the classpth:

I am compiling it with Netbeans (I don't think that this is really relevant, but I am reporting this anyway to make it perfectly and faithfully reproducible). The five JARs above are kept in a folder called "lib" inside the project folder (along with "src", "nbproject", "test" and "build"). I added them to Netbeans via the "Add JAR/Folder" button in the project properties and they are listed in the exact order as the list above. The project is a standard "Java application" type project.

Further, the Netbeans project is configured to "do NOT compile on save", "generate debugging info", "report deprecated APIs", "track java dependencies", "activacte annotation proccessing" and "activacte annotation proccessing in the editor". No annotation processor or annotation processing option is explicitly configured in Netbeans. Also, the "-Xlint:all" command line option is passed in the compiler command line, and the compiler runs on an external VM.

My javac's version is 1.8.0_72 and my java's version is 1.8.0_72-b15. My Netbeans is 8.1.

My project compiles fine. However, it throws an exception in its execution. The exception don't seems to be anything that looks easily or obvious fixable. Here is the output, including the stacktrace:

TestFoo(x=b, z=5)
{"z":5,"xoom":"a"}
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Argument #0 of constructor [constructor for testelombok.TestFoo, annotations: {interface java.beans.ConstructorProperties=@java.beans.ConstructorProperties(value=[x, z]), interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}] has no property name annotation; must have name when multiple-parameter constructor annotated as Creator
 at [Source: {"z":5,"xoom":"a"}; line: 1, column: 1]
    at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:296)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:269)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCacheValueDeserializer(DeserializerCache.java:244)
    at com.fasterxml.jackson.databind.deser.DeserializerCache.findValueDeserializer(DeserializerCache.java:142)
    at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:475)
    at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3890)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3785)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2833)
    at testelombok.TestLombok.main(TestLombok.java:14)
Caused by: java.lang.IllegalArgumentException: Argument #0 of constructor [constructor for testelombok.TestFoo, annotations: {interface java.beans.ConstructorProperties=@java.beans.ConstructorProperties(value=[x, z]), interface com.fasterxml.jackson.annotation.JsonCreator=@com.fasterxml.jackson.annotation.JsonCreator(mode=DEFAULT)}] has no property name annotation; must have name when multiple-parameter constructor annotated as Creator
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._addDeserializerConstructors(BasicDeserializerFactory.java:511)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory._constructDefaultValueInstantiator(BasicDeserializerFactory.java:323)
    at com.fasterxml.jackson.databind.deser.BasicDeserializerFactory.findValueInstantiator(BasicDeserializerFactory.java:253)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.buildBeanDeserializer(BeanDeserializerFactory.java:219)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerFactory.createBeanDeserializer(BeanDeserializerFactory.java:141)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer2(DeserializerCache.java:406)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createDeserializer(DeserializerCache.java:352)
    at com.fasterxml.jackson.databind.deser.DeserializerCache._createAndCache2(DeserializerCache.java:264)
    ... 7 more

I already tried about randomly poking with the @Value and @AllArgsConstructor annotations, but I couldn't make it any better.

I google'd the exception and found an old bug report on jackson, and another one that is open, but seems to be related to something else. However, this still do not tells anything about what is this bug or how to fix it. Also, I could not find anything useful looking that somewhere else.

Since what I am trying to do is very basic usage of both lombok and jackson, it seems odd that I couldn't find any more useful information about how to workaround this issue. Maybe I missed something?

Other than just saying "don't use lombok" or "don't use jackson", do anybody has any idea about how to solve this?

Codel answered 8/9, 2016 at 1:58 Comment(4)
Top answers to this question are a bit outdated. Please check out solution involving @Jacksonized annotation down below.Naumachia
this works for me - github.com/projectlombok/lombok/issues/…Liles
@Naumachia Top answers are a bit outdated, but an experimental feature will never go in any production environment.Commensal
First of all - quite a bold statement. And second, I was referring to @Jacksonized, which is not experimental.Naumachia
N
86

Immutable + Lombok + Jackson can be achieved in next way:

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.Value;

@Value
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class LocationDto {

    double longitude;
    double latitude;
}

class ImmutableWithLombok {

    public static void main(String[] args) throws Exception {
        ObjectMapper objectMapper = new ObjectMapper();

        String stringJsonRepresentation = objectMapper.writeValueAsString(new LocationDto(22.11, 33.33));
        System.out.println(stringJsonRepresentation);

        LocationDto locationDto = objectMapper.readValue(stringJsonRepresentation, LocationDto.class);
        System.out.println(locationDto);
    }
}
Niki answered 2/5, 2019 at 17:58 Comment(9)
Isn't AllArgsConstructor already a part of @Value annotation?Tadeas
Adding an explicit @NoArgsConstructor overrides the constructor which is generated by the @Value annotation, so you have to add the extra @AllArgsConstructorPyrogen
Best solution I've found - in my opinion - without Builder pattern. Thanks.Cyton
Is adding @AllArgsConstructor "does" the @JsonCreator on the constructor for us? If yes, why do we need the @NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)?Selfstyled
It is something I was looking for for a while! Replying to the last comment, you need both of them, that's why need @NoArgsConstructor but if you don't initialize the fields (force=true) you get an error when the @NoArgsConstructor annotation is set because fields are finalTilt
This works, but can anybody explain how can we still get an immutable class and NoArgsConstructor at the same time? If there are no setters, how Jackson proceeds to construct an object?Blunt
@Tadeas I tried with just @Value @AllArgsConstructor (without @NoArgsConstructor) and it works for me. If I put just @Value is not enough for jackson, so @AllArgsConstructor is doing something additional.Risibility
@Selfstyled in my case, @Value @AllArgsConstructor was enoughRisibility
You think that class is immutable?Pains
F
80

If you want immutable but a json serializable POJO using lombok and jackson. Use jacksons new annotation on your lomboks builder @JsonPOJOBuilder(withPrefix = "") I tried this solution and it works very well. Sample usage

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import lombok.Builder;
import lombok.Value;

@JsonDeserialize(builder = Detail.DetailBuilder.class)
@Value
@Builder
public class Detail {

    private String url;
    private String userName;
    private String password;
    private String scope;

    @JsonPOJOBuilder(withPrefix = "")
    public static class DetailBuilder {

    }
}

If you have too many classes with @Builder and you want don't want the boilerplate code empty annotation you can override the annotation interceptor to have empty withPrefix

mapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
        @Override
        public JsonPOJOBuilder.Value findPOJOBuilderConfig(AnnotatedClass ac) {
            if (ac.hasAnnotation(JsonPOJOBuilder.class)) {//If no annotation present use default as empty prefix
                return super.findPOJOBuilderConfig(ac);
            }
            return new JsonPOJOBuilder.Value("build", "");
        }
    });

And you can remove the empty builder class with @JsonPOJOBuilder annotation.

Fates answered 15/2, 2018 at 6:37 Comment(3)
This solution keeps immutability whilst working around Jackson's brain-dead constructor based deserialisation issues (multi-field constructor requires @JsonProperty on each constructor argument, rather than trying something intelligent). (I had tried onConstructor_={@JsonCreator} without luck)Penetrant
I am not sure, but I get the error Builder class does not have the build method.Mccraw
how do you do the same with inherited subclasses?? The @Builder doesn't support adding field values for the parent class fieldsPharisee
E
39

I tried several of the above and they were all temperamental. What really worked for me is the the answer I found here.

on your project's root directory add a lombok.config file (if you haven't done already)

lombok.config

and inside paste this

lombok.anyConstructor.addConstructorProperties=true

Then you can define your pojos like the following:

@Data
@AllArgsConstructor
public class MyPojo {

    @JsonProperty("Description")
    private String description;
    @JsonProperty("ErrorCode")
    private String errorCode;
}
Eohippus answered 13/5, 2019 at 18:39 Comment(6)
But isn't this mutable?Bobcat
to be immutable, just change data by Getter and Builder or RequiredArgsConstructor and mark all fields as finalDeclination
any ideas on why this is not working in my case? I have a spring boot application and I have placed lombok.config inside my src but still your solution is not working.Shroyer
This config did not solve the problem for me, at least not in my unit tests.Kessel
Use @Value instead of @Data to make it immutable.Ettaettari
Whilst this is a bad example because it's mutable - the property does help with OPs error. @AllArgsConstructor(onConstructor=@__(@JsonCreator)) and @Value will work but only if you also have addConstructorProperties set to true.Parashah
D
28

Here is an example by using @Jacksonized annotation:

import lombok.Value;
import lombok.Builder;
import lombok.extern.jackson.Jacksonized;

@Jacksonized
@Builder
@Value
public class User {
    String name;
    String surname;
}

It does require you to use @Builder annotation.

Danella answered 29/9, 2021 at 7:8 Comment(3)
I think @Value may not be strictly necessaryFrit
but regardless, I think this is the simplest Answer :)Frit
This works for final fields as well. By far the cleanest and simplest answer here, thanks!Anallese
C
8

I had exactly the same issue, "solved" it by adding the suppressConstructorProperties = true parameter (using your example):

@Value
@Wither
@AllArgsConstructor(suppressConstructorProperties = true)
public class TestFoo {
    @JsonProperty("xoom")
    private String x;
    private int z;
}

Jackson apparently does not like the java.beans.ConstructorProperties annotation added to constructors. The suppressConstructorProperties = true parameter tells Lombok not to add it (it does by default).

Catherinacatherine answered 19/10, 2016 at 11:25 Comment(2)
suppressConstructorProperties is now deprecated :-(Dawes
Which is not a problem, since the new default is also false.Kamchatka
T
8

It can be done simpler, without extra annotations and the problem can be with the inheritance, i.e. child classes should be deserializable as well. So, my example:

Requirements:

lombok.config inside the project root directory with body containing:

lombok.anyConstructor.addConstructorProperties=true
/** The parent class **/

@Value
@NonFinal
@SuperBuilder
@RequiredArgsConstructor
public class Animal {
  String name;
}

/** The child class **/

@Value
@SuperBuilder
@RequiredArgsConstructor
public class Cat {
  Long tailLength;
  
  @ConstructorProperties({"tailLength", "name})
  public Cat(Long tailLength, String name) {
      super(name);
      this.tailLength = tailLength;
  }
}

It:

  1. Allows building of objects including fields of the parent
  2. Serializes/Deserializes with the default ObjectMapper and Jackson
  3. Instances of the parent and children classes are immutable

My advice against other examples:

  1. Try not to put custom annotations on particular classes, it makes it inhomogeneous. Any way, you will come to a generic solution one day.
  2. Try not to put Jackson annotations on any fields on constructors, it creates coupling, when Jackson is capable to serialize/deserialize without any annotations.
  3. Do not use @AllArgsConstructor for immutable entities. When your class has only final fields, conceptually right is @RequiredArgsConstructor, that's how you guarantee that class-clients always will rely only on a constructor with the immutable entity. Will @AllArgsConstructor it might lead to passing nulls.
Trahern answered 15/6, 2021 at 10:47 Comment(1)
the best solution I've found, looks much clearer at leastAdapa
E
6

From Jan Rieke's Answer

Since lombok 1.18.4, you can configure what annotations are copied to the constructor parameters. Insert this into your lombok.config:

lombok.copyableAnnotations += com.fasterxml.jackson.annotation.JsonProperty

Then just add @JsonProperty to your fields:

...

You'll need a @JsonProperty on every field even if the name matches, but that is a good practice to follow anyway. You can also set your fields to public final using this, which I prefer over getters.

@ToString
@EqualsAndHashCode
@Wither
@AllArgsConstructor(onConstructor=@__(@JsonCreator))
public class TestFoo {
    @JsonProperty("xoom")
    public final String x;
    @JsonProperty("z")
    public final int z;
}

It should also work with getters (+setters) though.

Ernestoernestus answered 2/8, 2019 at 18:30 Comment(1)
This solves my issue! Thank you so much! I've been stuck on spring-boot-integration and serialization between JSON and POJOs and getting the error: Can not deserialize instance of java.lang.String out of START_OBJECT token... And this fixed it! I need to have that @JsonProperty annotation for each constructor parameter and that addition to the @AllArgsConstructor allows lombok to instrument that in.Negligee
K
5

I found two options to solve this problem if you want to use @Builder with Jackson.

Option 1

  • Add private default noArgs and allArgs constructors.
@Builder
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Person {

    @JsonProperty("user_name")
    private String name;
}

Option 2

Thanks to this article.

Jackson expects the builder methods to start like .withProperty(...) but Lombok generates .property(...).

What you can do is to create the builder class yourself so that you can add Jackson annotations to it. Lombok will then re-use this class and add all the builder methods to it.

@JsonDeserialize(builder = MyDto.MyDtoBuilder.class)
@Builder
@Getter
public class MyDto {

    @JsonProperty("user_id")
    private String userId;

    @JsonPOJOBuilder(withPrefix = "")
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class MyDtoBuilder {
    }
}
  • You need to do some manual work
  • Still much better than writing the Builder yourself
  • Also note that additional properties like @JsonIgnorePropertie go on the builder

An additional drawback is that refactorings will not automatically rename the MyDtoBuilder. I hope in a future Lombok/Jackson version this issue is solved.

Update: I've found another solution (tested with lombok 1.18.20 and spring boot 2.4.5), added as Option 1.

Kessel answered 1/5, 2021 at 8:48 Comment(0)
G
5

I have managed to keep my classes immutable and also deserialize them by using this lombok annotation:

@NoArgsConstructor(force = true)
Gradual answered 21/7, 2021 at 14:25 Comment(0)
E
4

@AllArgsConstructor(suppressConstructorProperties = true) is deprecated. Define lombok.anyConstructor.suppressConstructorProperties=true (https://projectlombok.org/features/configuration) and change POJO's lombok annotation from @Value to @Data + @NoArgsConstructor + @AllArgsConstructor works for me.

Entrust answered 28/12, 2017 at 1:39 Comment(1)
This changes the class to be mutable, which I assume is not what OP wantedMorell
E
3

I've all my classes annotated like this:

@JsonAutoDetect(fieldVisibility = Visibility.ANY)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@Data
@Accessors(fluent = true)
@NoArgsConstructor
@AllArgsConstructor

It worked with all Lombok and Jackson versions for, at least, a couple of years.

Example:

@JsonAutoDetect(fieldVisibility = Visibility.ANY)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@Data
@Accessors(fluent = true)
@NoArgsConstructor
@AllArgsConstructor
public class Person {
    String id;
    String first;
    String last;
}

And that's it. Lombok and Jackson play together like a charm.

Endocarp answered 29/3, 2017 at 5:56 Comment(1)
This is mutable class.August
A
3

For me it worked when I have updated lombok version to: 'org.projectlombok:lombok:1.18.0'

Anticipation answered 24/7, 2018 at 13:14 Comment(0)
S
1

You can get Jackson to play with just about anything if you use its "mixin" pattern. Basically, it gives you a way to add Jackson annotations onto an existing class without actually modifying that class. I'm leaning towards recommending it here rather than a Lombok solution because this is solves a problem Jackson is having with a Jackson feature, so it's more likely to work long-term.

Souvaine answered 8/9, 2016 at 4:52 Comment(0)
V
1

I would suggest you to use Gson as it does not give you all this hassle.

I added this in my spring boot app

spring.mvc.converters.preferred-json-mapper=gson

along with the dependency in maven and I solved all the problems. I didn't need to modify my lombok annotated pojos

Vulpecula answered 21/1, 2021 at 10:42 Comment(1)
Lombok team has created @Jacksonized annotation which generates all the code required from Jackson and it solved all my problems in a more clean way saving lot of time. Finally!Vulpecula
O
0
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class Person {
   String id;
   String first;
   String last;
}

Additional to the Data Class, it should be correct configured the ObjectMapper. In this case, it is working ok with a ParameterNamesModule configuration, and setting visibility of Fields and Creator Methods

    om.registerModule(new ParameterNamesModule());
    om.setVisibility(FIELD, JsonAutoDetect.Visibility.ANY);
    om.setVisibility(CREATOR, JsonAutoDetect.Visibility.ANY);

Then it should work as expected.

Obsolesce answered 10/10, 2017 at 9:52 Comment(0)
E
0

I was having issues with getting Lombok to not add the ConstructorProperies annotation so went the other way and disabled Jackson from looking at that annotation.

The culprit is JacksonAnnotationIntrospector.findCreatorAnnotation. Notice:

if (_cfgConstructorPropertiesImpliesCreator
            && config.isEnabled(MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES)

Also notice JacksonAnnotationIntrospector.setConstructorPropertiesImpliesCreator:

public JacksonAnnotationIntrospector setConstructorPropertiesImpliesCreator(boolean b)
{
    _cfgConstructorPropertiesImpliesCreator = b;
    return this;
}

So two options, either set the MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES to false or create a JacksonAnnotationIntrospector set setConstructorPropertiesImpliesCreator to false and set this AnnotationIntrospector into the ObjectMapper via ObjectMapper.setAnnotationIntrospector.

Notice a couple things, I am using Jackson 2.8.10 and in that version MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES does not exist. I am not sure in which version of Jackson it was added. So if it is not there, use the JacksonAnnotationIntrospector.setConstructorPropertiesImpliesCreator mechanism.

Excitable answered 14/5, 2018 at 13:39 Comment(0)
W
0

You need to have this module as well. https://github.com/FasterXML/jackson-modules-java8

then turn on -parameters flag for your compiler.

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
Weiman answered 21/5, 2018 at 21:56 Comment(0)
D
0

I struggled with this for a moment as well. But looking through the documentation here I can see that the onConstructor annotation parameter is considered experimental and is not supported well on my IDE (STS 4). According to the Jackson documentation, private members are not (de)serialized by default. There are quick ways to resolve this.

Add JsonAutoDetect annotation and set it appropriately to detect protected/private members. This is convenient for DTOs

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class SomeClass

Add a factory function with @JsonCreator annotation, this works best if you need some object validation or additional transforms.

public class SomeClass {

   // some code here

   @JsonCreator
   public static SomeClass factory(/* params here dressing them in @JsonProperty annotations*/) {
      return new SomeClass();
   }
}

Of course you could just manually add the constructor in yourself also as well.

Driggers answered 2/7, 2019 at 17:5 Comment(0)
C
0

Options which worked for me

  • This worked for me just by adding @AllArgsConstructor in my bean.
  • Add mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); object mapper instance.
Clambake answered 20/4, 2021 at 4:20 Comment(0)
C
0

None of the above answers worked for me, but this below did.

What happens is that Jackson does not support the fluent getters, BUT you can tell it to use reflection to read the fields.

Try this:

@Value
@Accessors(chain = true, fluent = true)
@Builder(builderClassName = "Builder")
public static class TestFoo {
  // ...
}

var foo = ...
var writer = new ObjectMapper()
    .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
    .writer();

writer.writeValueAsString(foo);
Carvel answered 19/9, 2022 at 10:48 Comment(0)
I
-2

I had a different issue and it was with the boolean primitive types.

private boolean isAggregate;

It was throwing the following error as a result

Exception: Unrecognized field "isAggregate" (class 

Lambok converts isAggregate to isAggregate() as a getter making the property internally to lombok as aggregate instead isAggregate. The Jackson library doesn't like it and it needs isAggregate property instead.

I updated the primitive boolean to Wrapper Boolean to work around this issue. There are other options for you if you are dealing with boolean types, see the reference below.

Sol:

private Boolean isAggregate;

ref: https://www.baeldung.com/lombok-getter-boolean

Igraine answered 1/10, 2019 at 18:23 Comment(1)
Did you try using aggregate instead of isAggregate with primitive type? I believe, it can resolve the error as well. Please also see this https://mcmap.net/q/94916/-lombok-annotation-getter-for-boolean-fieldMaggiore

© 2022 - 2024 — McMap. All rights reserved.