1. Introduction

In this tutorial, we’ll take a look at Moshi, a modern JSON library for Java that will give us powerful JSON serialization and deserialization in our code with little effort.

Moshi has a smaller API than other libraries like Jackson or Gson without compromising on functionality. This makes it easier to integrate into our applications and lets us write more testable code. It is also a smaller dependency, which may be important for certain scenarios – such as developing for Android.

2. Adding Moshi to Our Build

Before we can use it, we first need to add the Moshi JSON dependencies to our pom.xml file:

<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi</artifactId>
    <version>1.9.2</version>
</dependency>
<dependency>
    <groupId>com.squareup.moshi</groupId>
    <artifactId>moshi-adapters</artifactId>
    <version>1.9.2</version>
</dependency>

The com.squareup.moshi:moshi dependency is the main library, and the com.squareup.moshi:moshi-adapters dependency is some standard type adapters – which we’ll explore in more detail later.

3. Working with Moshi and JSON

Moshi allows us to convert any Java values into JSON and back again anywhere we need to for whatever reasons – e.g. for file storage, writing REST APIs, whatever needs we might have.

Moshi works with the concept of a JsonAdapter class. This is a typesafe mechanism to serialize a specific class into a JSON string and to deserialize a JSON string back into the correct type:

public class Post {
    private String title;
    private String author;
    private String text;
    // constructor, getters and setters
}

Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

Once we’ve built our JsonAdapter, we can use it whenever we need to in order to convert our values to JSON using the toJson() method:

Post post = new Post("My Post", "Baeldung", "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"Baeldung","text":"This is my post","title":"My Post"}

And, of course, we can convert JSON back into the expected Java types with the corresponding fromJson() method:

Post post = jsonAdapter.fromJson(json);
// new Post("My Post", "Baeldung", "This is my post");

4. Standard Java Types

Moshi comes with built-in support for standard Java types, converting to and from JSON exactly as expected. This covers:

In addition to these, Moshi will also automatically work with any arbitrary Java bean, converting this to a JSON object where the values are converted using the same rules as any other type. This obviously means that Java beans within Java beans are correctly serialized as deep as we need to go.

The moshi-adapters dependency then gives us access to some additional conversion rules, including:

  • A slightly more powerful adapter for Enums – supporting a fallback value when reading an unknown value from the JSON
  • An adapter for java.util.Date supporting the RFC-3339 format

Support for these needs to be registered with a Moshi instance before they’ll be used. We’ll see this exact pattern soon when we add support for our own custom types:

Moshi moshi = new Moshi.builder()
  .add(new Rfc3339DateJsonAdapter())
  .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class).withUnknownFallback(CurrencyCode.USD))
  .build()

5. Custom Types in Moshi

Everything so far has given us total support for serializing and deserializing any Java object into JSON and back. But this doesn’t give us much control over what the JSON looks like, serializing Java objects by literally writing every field in the object as-is. This works but is not always what we want.

Instead, we can write our own adapters for our own types and have exact control over how the serialization and deserialization of these types works.

5.1. Simple Conversions

The simple case is converting between a Java type and a JSON one – for example a string. This can be very useful when we need to represent complex data in a specific format.

For example, imagine we have a Java type representing the author of a post:

public class Author {
    private String name;
    private String email;
    // constructor, getters and setters
}

With no effort at all, this will serialize as a JSON object containing two fields – name and email. We want to serialize it as a single string though, combining the name and email address together.

We do this by writing a standard class that contains a method annotated with @ToJson:

public class AuthorAdapter {
    @ToJson
    public String toJson(Author author) {
        return author.name + " <" + author.email + ">";
    }
}

Obviously, we need to go the other way as well. We need to parse our string back into our Author object. This is done by adding a method annotated with @FromJson instead:

@FromJson
public Author fromJson(String author) {
    Pattern pattern = Pattern.compile("^(.*) <(.*)>$");
    Matcher matcher = pattern.matcher(author);
    return matcher.find() ? new Author(matcher.group(1), matcher.group(2)) : null;
}

Once done, we need to actually make use of this. We do this at the time we are creating our Moshi by adding the adapter to our Moshi.Builder:

Moshi moshi = new Moshi.Builder()
  .add(new AuthorAdapter())
  .build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

Now we can immediately start to convert these objects to and from JSON, and get the results that we wanted:

Post post = new Post("My Post", new Author("Baeldung", "[email protected]"), "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"Baeldung <[email protected]>","text":"This is my post","title":"My Post"}

Post post = jsonAdapter.fromJson(json);
// new Post("My Post", new Author("Baeldung", "[email protected]"), "This is my post");

5.2. Complex Conversions

These conversions have been between Java beans and JSON primitive types. We can also convert to structured JSON as well – essentially letting us convert a Java type to a different structure for rendering in our JSON.

For example, we might have a need to render a Date/Time value as three different values – the date, the time and the timezone.

Using Moshi, all we need to do is write a Java type representing the desired output and then our @ToJson method can return this new Java object, which Moshi will then convert to JSON using its standard rules:

public class JsonDateTime {
    private String date;
    private String time;
    private String timezone;

    // constructor, getters and setters
}
public class JsonDateTimeAdapter {
    @ToJson
    public JsonDateTime toJson(ZonedDateTime input) {
        String date = input.toLocalDate().toString();
        String time = input.toLocalTime().toString();
        String timezone = input.getZone().toString();
        return new JsonDateTime(date, time, timezone);
    }
}

As we can expect, going the other way is done by writing an @FromJson method that takes our new JSON structured type and returns our desired one:

@FromJson
public ZonedDateTime fromJson(JsonDateTime input) {
    LocalDate date = LocalDate.parse(input.getDate());
    LocalTime time = LocalTime.parse(input.getTime());
    ZoneId timezone = ZoneId.of(input.getTimezone());
    return ZonedDateTime.of(date, time, timezone);
}

We are then able to use this exactly as above to convert our ZonedDateTime into our structured output and back:

Moshi moshi = new Moshi.Builder()
  .add(new JsonDateTimeAdapter())
  .build();
JsonAdapter<ZonedDateTime> jsonAdapter = moshi.adapter(ZonedDateTime.class);

String json = jsonAdapter.toJson(ZonedDateTime.now());
// {"date":"2020-02-17","time":"07:53:27.064","timezone":"Europe/London"}

ZonedDateTime now = jsonAdapter.fromJson(json);
// 2020-02-17T07:53:27.064Z[Europe/London]

5.3. Alternative Type Adapters

Sometimes we want to use an alternative adapter for a single field, as opposed to basing it on the type of the field.

For example, we might have a single case where we need to render date and time as milliseconds from the epoch instead of as an ISO-8601 string.

Moshi lets us do this by the use of a specially-annotated annotation which we can then apply both to our field and our adapter:

@Retention(RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@JsonQualifier
public @interface EpochMillis {}

The key part of this is the @JsonQualifier annotation, which allows Moshi to tie any fields annotated with this to the appropriate Adapter methods.

Next, we need to write an adapter. As always we have both a @FromJson and a @ToJson method to convert between our type and JSON:

public class EpochMillisAdapter {
    @ToJson
    public Long toJson(@EpochMillis Instant input) {
        return input.toEpochMilli();
    }
    @FromJson
    @EpochMillis
    public Instant fromJson(Long input) {
        return Instant.ofEpochMilli(input);
    }
}

Here, we’ve used our annotation on the input parameter to the @ToJson method and on the return value of the @FromJson method.

Moshi can now use this adapter or any field that is also annotated with @EpochMillis:

public class Post {
    private String title;
    private String author;
    @EpochMillis Instant posted;
    // constructor, getters and setters
}

We are now able to convert our annotated type to JSON and back as needed:

Moshi moshi = new Moshi.Builder()
  .add(new EpochMillisAdapter())
  .build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

String json = jsonAdapter.toJson(new Post("Introduction to Moshi Json", "Baeldung", Instant.now()));
// {"author":"Baeldung","posted":1582095384793,"title":"Introduction to Moshi Json"}

Post post = jsonAdapter.fromJson(json);
// new Post("Introduction to Moshi Json", "Baeldung", Instant.now())

6. Advanced JSON Processing

Now that we can convert our types to JSON and back, and we can control the way that this conversion happens. There are some more advanced things that we may need to do on occasion with our processing though, which Moshi makes easy to achieve.

6.1. Renaming JSON Fields

On occasion, we need our JSON to have different field names to our Java beans. This may be as simple as wanting camelCase in Java and snake_case in JSON, or it might be to completely rename the field to match the desired schema.

We can use the @Json annotation to give a new name to any field in any bean that we control:

public class Post {
    private String title;
    @Json(name = "authored_by")
    private String author;
    // constructor, getters and setters
}

Once we’ve done this, Moshi immediately understands that this field has a different name in the JSON:

Moshi moshi = new Moshi.Builder()
  .build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

Post post = new Post("My Post", "Baeldung");

String json = jsonAdapter.toJson(post);
// {"authored_by":"Baeldung","title":"My Post"}

Post post = jsonAdapter.fromJson(json);
// new Post("My Post", "Baeldung")

6.2. Transient Fields

In certain cases, we may have fields that should not be included in the JSON. Moshi uses the standard transient qualifier to indicate that these fields are not to be serialized or deserialized:

public static class Post {
    private String title;
    private transient String author;
    // constructor, getters and setters
}

We will then see that this field is completely ignored both when serializing and deserializing:

Moshi moshi = new Moshi.Builder()
  .build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

Post post = new Post("My Post", "Baeldung");

String json = jsonAdapter.toJson(post);
// {"title":"My Post"}

Post post = jsonAdapter.fromJson(json);
// new Post("My Post", null)

Post post = jsonAdapter.fromJson("{\"author\":\"Baeldung\",\"title\":\"My Post\"}");
// new Post("My Post", null)

6.3. Default Values

Sometimes we are parsing JSON that does not contain values for every field in our Java Bean. This is fine, and Moshi will do its best to do the right thing.

Moshi is not able to use any form of argument constructor when deserializing our JSON, but it is able to use a no-args constructor if one is present.

This will then allow us to pre-populate our bean before the JSON is serialized, giving any required default values to our fields:

public class Post {
    private String title;
    private String author;
    private String posted;

    public Post() {
        posted = Instant.now().toString();
    }
    // getters and setters
}

If our parsed JSON is lacking the title or author fields then these will end up with the value null. If we are lacking the posted field then this will instead have the current date and time:

Moshi moshi = new Moshi.Builder()
  .build();
JsonAdapter<Post> jsonAdapter = moshi.adapter(Post.class);

String json = "{\"title\":\"My Post\"}";
Post post = jsonAdapter.fromJson(json);
// new Post("My Post", null, "2020-02-19T07:27:01.141Z");

6.4. Parsing JSON Arrays

Everything that we’ve done so far has assumed that we are serializing and deserializing a single JSON object into a single Java bean. This is a very common case, but it’s not the only case. Sometimes we want to also work with collections of values, which are represented as an array in our JSON.

When the array is nested inside of our beans, there’s nothing to do. Moshi will just work. When the entire JSON is an array then we have to do more work to achieve this, simply because of some limitations in Java generics. We need to construct our JsonAdapter in a way that it knows it is deserializing a generic collection, as well as what the collection is.

Moshi offers some help to construct a java.lang.reflect.Type that we can provide to the JsonAdapter when we build it so that we can provide this additional generic information:

Moshi moshi = new Moshi.Builder()
  .build();
Type type = Types.newParameterizedType(List.class, String.class);
JsonAdapter<List<String>> jsonAdapter = moshi.adapter(type);

Once this is done, our adapter works exactly as expected, honoring these new generic bounds:

String json = jsonAdapter.toJson(Arrays.asList("One", "Two", "Three"));
// ["One", "Two", "Three"]

List<String> result = jsonAdapter.fromJson(json);
// Arrays.asList("One", "Two", "Three");

7. Summary

We’ve seen how the Moshi library can make converting Java classes to and from JSON really easy, and how flexible it is. We can use this library anywhere that we need to convert between Java and JSON – whether that’s loading and saving from files, database columns or even REST APIs. Why not try it out?

As usual, the source code for this article can be found over on GitHub.


» 下一篇: Spring AMQP 重试