ObjectMapper - Best practice for thread-safety and performance
Asked Answered
L

3

26

Summary

I want to find the best practice for using ObjectMapper and/or ObjectReader in terms of thread-safety and performance in the context of the use-case described below.

Background

I have a helper class (Json.java) where the method toObject() uses ObjectMapper to translate from a json string to an object of a given (json-mappable) class.

Problem / question

I have read that ObjectReader is often recommended for being fully thread-safe, but I mostly see it being in a non-generic context where the class to read for is predefined. In this context, what do you believe to be the best practice in terms of thread-safety and performance? In the code I have three suggestions that I could think of as a starting point.

I have tried to look at through the source code and docs for jackson-databind, but my theoretical Java skills are not good enough to derive an answer from them. I have also looked at similar questions on SO and elsewhere, but haven't found any that match my case closely enough.

import java.io.IOException;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;

public abstract class Json {

    private static final ObjectMapper jsonMapper = new ObjectMapper();
    
    // NOTE: jsonReader is only relevant for Suggestion 3.
    private static final ObjectReader jsonReader = jsonMapper.reader(); 

    // Suggestion 1:
    public static <T> T toObject1(final Class<T> type, final String json) throws IOException {
        return jsonMapper.readValue(json, type);
    }

    // Suggestion 2:
    public static <T> T toObject2(final Class<T> type, final String json) throws IOException {
        return jsonMapper.readerFor(type).readValue(json);
    }

    // Suggestion 3:
    public static <T> T toObject3(final Class<T> type, final String json) throws IOException {
        return jsonReader.forType(type).readValue(json);
    }

    // Remainder of class omitted for brevity.
}
Lyndsaylyndsey answered 27/8, 2019 at 8:20 Comment(2)
I have always used suggestion #1. This lets the mapper sort out the best way to perform de-serializationDiatessaron
@SharonBenAsher why? how does it make a difference?Overlap
O
23
private static final ObjectMapper jsonMapper = new ObjectMapper();

Constructing an ObjectMapper instance is a relatively expensive operation, so it's recommended to create one object and reuse it. You did it right making it final.

// Suggestion 1:
public static <T> T toObject1(final Class<T> type, final String json) throws IOException {
    return jsonMapper.readValue(json, type);
}

You always read JSON to a POJO, so let's be precise and clear, and use ObjectReader.

// Suggestion 2:
public static <T> T toObject2(final Class<T> type, final String json) throws IOException {
    return jsonMapper.readerFor(type).readValue(json);
}

// Suggestion 3:
public static <T> T toObject3(final Class<T> type, final String json) throws IOException {
    return jsonReader.forType(type).readValue(json);
}

There is no difference, really. Both methods will construct a new ObjectReader object: the former (jsonMapper.readerFor(type)) will give you a fully-built instance directly, the latter (jsonReader.forType(type)) will complement the not-yet-usable jsonReader and returns a ready-to-use object. I would rather go with option 2 because I don't want to keep that field.

You shouldn't worry about performance or thread-safety. Even though creating an ObjectMapper might be costly (or making a copy out of it), getting and working with ObjectReaders is lightweight and completely thread-safe.

From the Java documentation (emphasis mine):

Uses "mutant factory" pattern so that instances are immutable (and thus fully thread-safe with no external synchronization); new instances are constructed for different configurations. Instances are initially constructed by ObjectMapper and can be reused, shared, cached; both because of thread-safety and because instances are relatively light-weight.

I recently had these questions myself and decided on ObjectMapper#reader(InjectableValues) as a factory method. It's very handy particularly when you want to customise an ObjectReader slightly or, as it was in my case, to adjust a DeserializationContext.

That's an excellent question, by the way.

Overlap answered 27/8, 2019 at 9:19 Comment(2)
Wow, thanks for the detailed answer! Your recommendation differs from those given by @davidxxx and Sharon Ben Asher, but it seems like you have really done your research here so I'm inclined to believe you.Lyndsaylyndsey
@Lyndsaylyndsey their points are good, and I think either option is fine (performant/thread-safe)Overlap
G
5

About concurrency

ObjectMapper versus ObjectReader is not relevant here.
The ObjectReader doesn't look to be helpful for your scenario.
Its specification says :

Builder object that can be used for per-serialization configuration of deserialization parameters, such as root type to use or object to update (instead of constructing new instance).

Note that both instances of ObjectMapper and ObjectReader are thread safe provided that their configuration is not changed between serialization/deserialization client calls.
The ObjectReader specified indeed :

Mapper instances are fully thread-safe provided that ALL configuration of the instance occurs before ANY read or write calls.

While ObjectReader has as difference to be immutable in the way where updating its configuration will make it return a new instance of as stated by its documentation :

Uses "mutant factory" pattern so that instances are immutable (and thus fully thread-safe with no external synchronization); new instances are constructed for different configurations.

In your requirement, you don't want to change the configuration between client calls. So using ObjectMapper looks more relevant.
So I would eliminate the 3) way and also the 2) way since jsonMapper.readerFor(type) that is factory method for ObjectReader instance. Still you don't matter to use an ObjectReader here.

So the simplest and common way looks better :

// Suggestion 1:
public static <T> T toObject1(final Class<T> type, final String json) throws IOException {
    return jsonMapper.readValue(json, type);
 }

About performance

Besides, remember ObjectReader is immutable. So the 2 and 3 ways create new instances of ObjectReader at each call. It doesn't look a good hint for performance.
Yes, these are lightweight objects but creating them at each time has a cost.
The ObjectReader doc says :

Instances are initially constructed by ObjectMapper and can be reused, shared, cached; both because of thread-safety and because instances are relatively light-weight.

There you don't reuse these instances. So you lose any benefit in terms of caching and of performance.
You could store them into a Map field and reuse them but do it only if you need to improve the actual performance of ObjectMapper and of course measure before concluding anything.

Conclusion : for your use case, I think that performance as well as concurrency is better with the first solution (ObjectMapper)

Grassy answered 27/8, 2019 at 8:51 Comment(7)
Great answer (I think)! So, essentially I should only be using ObjectReader if I want to apply different configurations for the different calls?Lyndsaylyndsey
That may be also helpful for other cases. As the javadoc says : "Builder object that can be used for per-serialization configuration of deserialization parameters, such as root type to use or object to update (instead of constructing new instance)." For example ObjectReader.withValueToUpdate(Object) is a good reason to use that. ttpshttps://mcmap.net/q/536961/-jackson-gson-apply-map-to-pojo Here you don't enter in this kind of scenarios.Grassy
@Grassy I guess "Instances are initially constructed by ObjectMapper" means "you normally use an ObjectMapper to create instances". It doesn't imply an ObjectMapper internally manages (e.g. caches) these instances. As a matter of fact, I couldn't find ObjectReader stored in ObjectMapper...Overlap
@Andrew Tobilko Of course. And it would be undesirable. My point is that creating ObjectReader at each call has a cost since it is not cached. So in the current scenario, it looks unnecessary.Grassy
@Grassy it would be quite desirable (for the same configuration), wouldn't it?Overlap
@Grassy my point is ObjectMapper doesn't cache anything either. By "anything" I meant any instances of ObjectReader\WriterOverlap
@Andrew Tobilko Of course but if both don't cache, what is the approach that looks more simple and efficient ? Creating an ObjectReader at each call or reusing the ObjectMapper ?Grassy
D
4

As I mention in the comment, I have always used suggestion #1. I have no knowledge if there is difference between the options in terms of thread safety/performance or at all.

However, this approach will not work if the target type is itself parameterized with generic type. The most obvious usage is some collection:

Json.toObject1(List.class, str);  // will deserialize into List<Object>

for this purpose you will have to use Jackson's TypeReference

// Suggestion 4:
public static <T> T toObject4(final TypeReference<T> typeRef, final String json) throws IOException {
    return jsonMapper.readValue(json, typeRef);
}

Json.toObject4(new TypeReference<List<SomeClass>>(){}, str);  // will deserialize into List<SomeClass>
Diatessaron answered 27/8, 2019 at 8:48 Comment(1)
Thanks for your answer! I was actually aware of the TypeReference case and had an overloaded toObject() for just that, but decided not to include it in the question for brevity. To clarify, I don't believe thread-safety or performance will actually be significant in what I'm working on here, but I find the library fascinating and want to gain a better understanding of it.Lyndsaylyndsey

© 2022 - 2024 — McMap. All rights reserved.