The purpose of this post is to show a relatively standard Jackson ObjectMapper configuration and show various ways to read / write json.
Configuration
Keep in mind it is a best practice to use ObjectMappers as a signleton. Shown is a static global using static methods for reading / writing json. Feel free to use Dependency Injection to manage your ObjectMapper singleton or write a wapper class that contains it. In rare cases you may wan't more than one ObjectMapper. For instance if an external API follows different standards than you it might be best to have two ObjectMappers. The anti-pattern is creating a new ObjectMapper every serialization request (Don't do that!).
private static final Json DEFAULT_SERIALIZER;
static {
ObjectMapper mapper = new ObjectMapper();
// Don't throw an exception when json has extra fields you are
// not serializing on. This is useful when you want to use a pojo
// for deserialization and only care about a portion of the json
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Ignore null values when writing json.
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
mapper.setSerializationInclusion(Include.NON_NULL);
// Write times as a String instead of a Long so its human readable.
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(new JavaTimeModule());
// Custom serializer for coda hale metrics.
mapper.registerModule(new MetricsModule(TimeUnit.MINUTES, TimeUnit.MILLISECONDS, false));
DEFAULT_SERIALIZER = new Json(mapper);
}
Serializing
Assume we want to write the following json.
{
"date": "2017-01-01",
"message": "Happy New Year!"
}
We have a POJO to represent this data mode.
private static class Message {
private final LocalDate date;
private final String message;
public Message(@JsonProperty("date") LocalDate date, @JsonProperty("message") String message) {
super();
this.date = date;
this.message = message;
}
public LocalDate getDate() {
return date;
}
public String getMessage() {
return message;
}
We can serialize by calling the following.
Message message = new Message(LocalDate.of(2017, 1, 1), "Happy New Year!");
String json = Json.serializer().toString(message);
This is achieved in the Json class by delegating to Jackson.
public String toString(Object obj) {
try {
return writer.writeValueAsString(obj);
} catch (IOException e) {
throw new JsonException(e);
}
}
Serializing to Pretty String using the PrettyWrtier
public String toPrettyString(Object obj) {
try {
return prettyWriter.writeValueAsString(obj);
} catch (IOException e) {
throw new JsonException(e);
}
}
Serializing to byte[]
public byte[] toByteArray(Object obj) {
try {
return prettyWriter.writeValueAsBytes(obj);
} catch (IOException e) {
throw new JsonException(e);
}
}
Deserializing
Using the same model from above we can deserialze with the following lines.
String expectedJson = Resources.asString("json-test/full-message.json");
Message expectedMessage = Json.serializer().fromJson(expectedJson, new TypeReference<Message>() {});
This is achieved in the Json class by delegating to Jackson.
public <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return mapper.readValue(json, typeRef);
} catch (IOException e) {
throw new JsonException(e);
}
}
Deserialize from byte[]
public <T> T fromJson(byte[] bytes, TypeReference<T> typeRef) {
try {
return mapper.readValue(bytes, typeRef);
} catch (IOException e) {
throw new JsonException(e);
}
}
Deserialize from InputStream
public <T> T fromInputStream(InputStream is, TypeReference<T> typeRef) {
try {
return mapper.readValue(is, typeRef);
} catch (IOException e) {
throw new JsonException(e);
}
}
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
Often times you may only need a certian portion of the JSON, or maybe you want to plan for the future where you may add new JSON fields on some objects. In these cases you should add the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
and set it to false as shown above.. This will make sure Jackson doesn't crash when there are extra json fields.
{
"date": "2017-01-01",
"message": "Happy New Year!",
"extra": []
}
We can use the same code above to parse this JSON object that has an additional field
String rawJson = Resources.asString("json-test/extra-fields.json");
Message message = Json.serializer().fromJson(rawJson, new TypeReference<Message>() {});
JsonNode's and Nested Objects
If you are working with a large / complex / deep nested JSON object and don't feel like creating a POJO to match it exactly you can use JsonNode
.
{
"foo": "test",
"nested1": {
"bar": "test",
"nested2": {
"baz": "test",
"nested3": {
"message": "Nested!"
}
}
}
}
Here we are interested in the nested3
object. We can quikcly grab it utilizing JsonNode.path
.
String rawJson = Resources.asString("json-test/nested.json");
JsonNode node = Json.serializer()
.nodeFromJson(rawJson)
.path("nested1")
.path("nested2")
.path("nested3");
Message message = Json.serializer().fromNode(node, new TypeReference<Message>() {});
Ignoring Null values
Jackson will write null fields by default for both Maps and POJOs. Use mapper.setSerializationInclusion(Include.NON_NULL)
to ignore null values for POJOs and SerializationFeature.WRITE_NULL_MAP_VALUES
set to false for Maps.