Introduction à Moshi Json

1. Introduction

Dans ce didacticiel, nous allons jeter un œil à Moshi, une bibliothèque JSON moderne pour Java qui nous donnera une sérialisation et une désérialisation JSON puissantes dans notre code avec peu d'effort.

Moshi a une API plus petite que d'autres bibliothèques comme Jackson ou Gson sans compromettre la fonctionnalité. Cela facilite l'intégration dans nos applications et nous permet d'écrire du code plus testable. Il s'agit également d'une dépendance plus petite, ce qui peut être important pour certains scénarios, tels que le développement pour Android.

2. Ajout de Moshi à notre build

Avant de pouvoir l'utiliser, nous devons d'abord ajouter les dépendances Moshi JSON à notre fichier pom.xml :

 com.squareup.moshi moshi 1.9.2   com.squareup.moshi moshi-adapters 1.9.2 

La dépendance com.squareup.moshi: moshi est la bibliothèque principale, et la dépendance com.squareup.moshi: moshi-adapters est des adaptateurs de type standard - que nous explorerons plus en détail plus tard.

3. Travailler avec Moshi et JSON

Moshi nous permet de convertir toutes les valeurs Java en JSON et inversement partout où nous en avons besoin pour toutes les raisons - par exemple pour le stockage de fichiers, l'écriture d'API REST, quels que soient les besoins que nous pourrions avoir.

Moshi fonctionne avec le concept d'une classe JsonAdapter . Il s'agit d'un mécanisme de type sécurisé pour sérialiser une classe spécifique dans une chaîne JSON et pour désérialiser une chaîne JSON dans le type correct:

public class Post { private String title; private String author; private String text; // constructor, getters and setters } Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(Post.class);

Une fois que nous avons construit notre JsonAdapter , nous pouvons l'utiliser chaque fois que nous en avons besoin afin de convertir nos valeurs en JSON en utilisant la méthode toJson () :

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"}

Et, bien sûr, nous pouvons reconvertir JSON dans les types Java attendus avec la méthode fromJson () correspondante :

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

4. Types Java standard

Moshi est livré avec un support intégré pour les types Java standard, convertissant vers et depuis JSON exactement comme prévu. Cela couvre:

  • Toutes les primitives - int, float, char , etc.
  • Tous les équivalents en boîte Java - Integer, Float, Character , etc.
  • Chaîne
  • Enums
  • Tableaux de ces types
  • Collections Java standard de ces types - List, Set, Map

En plus de cela, Moshi fonctionnera également automatiquement avec tout bean Java arbitraire, en le convertissant en un objet JSON où les valeurs sont converties en utilisant les mêmes règles que tout autre type. Cela signifie évidemment que les beans Java dans les beans Java sont correctement sérialisés aussi profondément que nécessaire.

La dépendance moshi-adapters nous donne alors accès à quelques règles de conversion supplémentaires, notamment:

  • Un adaptateur légèrement plus puissant pour Enums - prenant en charge une valeur de repli lors de la lecture d'une valeur inconnue à partir du JSON
  • Un adaptateur pour java.util.Date prenant en charge le format RFC-3339

La prise en charge de ceux-ci doit être enregistrée auprès d'une instance Moshi avant de pouvoir être utilisée. Nous verrons bientôt ce modèle exact lorsque nous ajouterons la prise en charge de nos propres types personnalisés:

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

5. Types personnalisés dans Moshi

Jusqu'à présent, tout nous a apporté un support total pour la sérialisation et la désérialisation de tout objet Java en JSON et inversement. Mais cela ne nous donne pas beaucoup de contrôle sur l'apparence du JSON, sérialisant les objets Java en écrivant littéralement chaque champ de l'objet tel quel. Cela fonctionne mais ce n'est pas toujours ce que nous voulons.

Au lieu de cela, nous pouvons écrire nos propres adaptateurs pour nos propres types et avoir un contrôle exact sur le fonctionnement de la sérialisation et de la désérialisation de ces types.

5.1. Conversions simples

Le cas simple est la conversion entre un type Java et un type JSON - par exemple une chaîne. Cela peut être très utile lorsque nous devons représenter des données complexes dans un format spécifique.

Par exemple, imaginons que nous ayons un type Java représentant l'auteur d'un article:

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

Sans effort du tout, cela sera sérialisé en tant qu'objet JSON contenant deux champs - nom et email . Nous voulons cependant le sérialiser sous forme de chaîne unique, combinant le nom et l'adresse e-mail ensemble.

Nous faisons cela en écrivant une classe standard qui contient une méthode annotée avec @ToJson :

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

De toute évidence, nous devons également aller dans l'autre sens. Nous devons analyser notre chaîne dans notre objet Author . Cela se fait en ajoutant une méthode annotée avec @FromJson à la place:

@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; }

Une fois cela fait, nous devons réellement en faire usage. Nous faisons cela au moment où nous créons notre Moshi en ajoutant l'adaptateur à notre Moshi.Builder :

Moshi moshi = new Moshi.Builder() .add(new AuthorAdapter()) .build(); JsonAdapter 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 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 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 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 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 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
    
      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 result = jsonAdapter.fromJson(json); // Arrays.asList("One", "Two", "Three");

7. Summary

Nous avons vu comment la bibliothèque Moshi peut rendre la conversion de classes Java vers et depuis JSON vraiment facile, et à quel point elle est flexible. Nous pouvons utiliser cette bibliothèque partout où nous avons besoin de convertir entre Java et JSON - qu'il s'agisse de chargement et d'enregistrement à partir de fichiers, de colonnes de base de données ou même d'API REST. Pourquoi ne pas l'essayer?

Comme d'habitude, le code source de cet article se trouve à l'adresse over sur GitHub.