«1. Введение

В этом руководстве мы рассмотрим Moshi, современную библиотеку JSON для Java, которая без особых усилий предоставит нам мощную сериализацию и десериализацию JSON в нашем коде.

Moshi имеет меньший API, чем другие библиотеки, такие как Jackson или Gson, без ущерба для функциональности. Это упрощает интеграцию в наши приложения и позволяет нам писать более тестируемый код. Это также меньшая зависимость, которая может быть важна для определенных сценариев, таких как разработка для Android.

2. Добавление Moshi в нашу сборку

Прежде чем мы сможем его использовать, нам сначала нужно добавить JSON-зависимости Moshi в наш файл pom.xml:

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

Зависимость com.squareup.moshi:moshi является основной библиотекой, а зависимость com.squareup.moshi:moshi-adapters — некоторыми адаптерами стандартного типа, которые мы рассмотрим более подробно позже.

3. Работа с Moshi и JSON

Moshi позволяет нам преобразовывать любые значения Java в JSON и обратно в любом месте, где это необходимо, по любым причинам — например, в для хранения файлов, написания REST API, любых наших потребностей.

Moshi работает с концепцией класса JsonAdapter. Это типобезопасный механизм для сериализации определенного класса в строку JSON и десериализации строки JSON обратно в правильный тип:

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);

После того, как мы создали наш JsonAdapter, мы можем использовать его, когда нам нужно, чтобы чтобы преобразовать наши значения в JSON с помощью метода 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"}

И, конечно же, мы можем преобразовать JSON обратно в ожидаемые типы Java с помощью соответствующего метода fromJson():

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

4. Стандартные типы Java

Moshi поставляется со встроенной поддержкой стандартных типов Java, конвертируя в JSON и из него точно так, как ожидалось. Сюда входят:

    Все примитивы — int, float, char и т. д. Все стандартные эквиваленты Java — Integer, Float, Character и т. д. Строковые перечисления Массивы этих типов Стандартные коллекции Java этих типов — List, Set, Map

В дополнение к этому, Moshi также будет автоматически работать с любым произвольным компонентом Java, преобразовывая его в объект JSON, где значения преобразуются с использованием тех же правил, что и для любого другого типа. Это, очевидно, означает, что Java-бины внутри Java-бинов правильно сериализуются настолько глубоко, насколько нам нужно.

Зависимость moshi-adapters затем дает нам доступ к некоторым дополнительным правилам преобразования, в том числе:

    Чуть более мощный адаптер для Enums — поддержка резервного значения при чтении неизвестного значения из JSON Адаптер для java. util.Date, поддерживающий формат RFC-3339

Их поддержку необходимо зарегистрировать в экземпляре Moshi, прежде чем они будут использоваться. Мы скоро увидим именно этот шаблон, когда добавим поддержку наших собственных типов: и назад. Но это не дает нам большого контроля над тем, как выглядит JSON, сериализуя объекты Java, буквально записывая каждое поле в объекте как есть. Это работает, но не всегда то, что нам нужно.

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

Вместо этого мы можем написать свои собственные адаптеры для наших собственных типов и иметь точный контроль над тем, как работает сериализация и десериализация этих типов.

5.1. Простые преобразования

Простым случаем является преобразование между типом Java и типом JSON, например, строкой. Это может быть очень полезно, когда нам нужно представить сложные данные в определенном формате.

Например, представьте, что у нас есть тип Java, представляющий автора сообщения:

Без каких-либо усилий это будет сериализовано как объект JSON, содержащий два поля — имя и адрес электронной почты. Однако мы хотим сериализовать его как одну строку, объединив имя и адрес электронной почты вместе.

Мы делаем это, написав стандартный класс, содержащий метод, аннотированный @ToJson:

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

Очевидно, нам нужно пойти и другим путем. Нам нужно разобрать нашу строку обратно в наш объект Author. Это делается путем добавления вместо этого метода с аннотацией @FromJson:

«

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

«После этого нам нужно использовать это. Мы делаем это во время создания нашего Moshi, добавляя адаптер в наш Moshi.Builder:

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

Теперь мы можем сразу начать преобразовывать эти объекты в JSON и из него и получать желаемые результаты: ~ ~~

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

5.2. Сложные преобразования

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");

Эти преобразования были между Java bean-компонентами и примитивными типами JSON. Мы также можем преобразовать в структурированный JSON, что, по сути, позволяет нам преобразовать тип Java в другую структуру для рендеринга в нашем JSON.

Например, нам может понадобиться отобразить значение даты/времени в виде трех разных значений — даты, времени и часового пояса.

Используя Moshi, все, что нам нужно сделать, это написать тип Java, представляющий желаемый результат, а затем наш метод @ToJson может вернуть этот новый объект Java, который Moshi затем преобразует в JSON, используя свои стандартные правила:


~~ ~ Как и следовало ожидать, пойти другим путем можно путем написания метода @FromJson, который берет наш новый структурированный тип JSON и возвращает желаемый:

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

Затем мы можем использовать его точно так же, как указано выше, для преобразования ZonedDateTime в наш структурированный вывод и обратно:

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

5.3. Адаптеры альтернативного типа

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]

Иногда мы хотим использовать альтернативный адаптер для одного поля, а не основывать его на типе поля.

Например, у нас может быть единственный случай, когда нам нужно отображать дату и время в миллисекундах от эпохи, а не в виде строки ISO-8601.

Moshi позволяет нам сделать это с помощью специально аннотированной аннотации, которую мы затем можем применить как к нашему полю, так и к нашему адаптеру:

Ключевой частью этого является аннотация @JsonQualifier, которая позволяет Moshi чтобы связать любые поля, аннотированные с помощью this, с соответствующими методами адаптера.

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

Далее нам нужно написать адаптер. Как всегда, у нас есть метод @FromJson и @ToJson для преобразования между нашим типом и JSON:

Здесь мы использовали нашу аннотацию для входного параметра метода @ToJson и для возвращаемого значения метод @FromJson.

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

Moshi теперь может использовать этот адаптер или любое поле, которое также аннотировано @EpochMillis:

Теперь мы можем преобразовать наш аннотированный тип в JSON и обратно по мере необходимости:

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

6. Расширенная обработка JSON

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())

Теперь, когда мы можем преобразовывать наши типы в JSON и обратно, мы можем контролировать, как происходит это преобразование. Однако есть некоторые более сложные вещи, которые нам, возможно, придется делать с нашей обработкой, и с которыми Moshi легко справляется.

6.1. Переименование полей JSON

Иногда нам нужно, чтобы наш JSON имел имена полей, отличные от наших Java-бинов. Это может быть так же просто, как хотеть camelCase в Java и snake_case в JSON, или это может быть полное переименование поля в соответствии с желаемой схемой.

Мы можем использовать аннотацию @Json, чтобы дать новое имя любому полю в любом bean-компоненте, которым мы управляем:

Как только мы это сделали, Moshi сразу же понимает, что это поле имеет другое имя в JSON:

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

6.2. Временные поля

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

В некоторых случаях у нас могут быть поля, которые не следует включать в JSON. Moshi использует стандартный квалификатор transient, чтобы указать, что эти поля не должны быть сериализованы или десериализованы:

Затем мы увидим, что это поле полностью игнорируется как при сериализации, так и при десериализации:

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

6.3. Значения по умолчанию

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)

Иногда мы анализируем JSON, который не содержит значений для каждого поля в нашем Java Bean. Это нормально, и Moshi сделает все возможное, чтобы поступить правильно.

Moshi не может использовать какую-либо форму конструктора аргументов при десериализации нашего JSON, но может использовать конструктор без аргументов, если он присутствует.

Это позволит нам предварительно заполнить наш bean-компонент до сериализации JSON, задав любые необходимые значения по умолчанию для наших полей:

Если в нашем проанализированном JSON отсутствуют поля заголовка или автора, то они завершатся вверх со значением null. Если у нас отсутствует поле публикации, то вместо него будут текущие дата и время:

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

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

6.4. Разбор массивов JSON

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");

«Все, что мы делали до сих пор, предполагало, что мы сериализуем и десериализуем один объект JSON в один Java-бин. Это очень распространенный случай, но не единственный. Иногда мы также хотим работать с коллекциями значений, которые представлены в виде массива в нашем JSON.

Когда массив вложен в наши bean-компоненты, делать нечего. Моши будет просто работать. Когда весь JSON представляет собой массив, нам нужно проделать больше работы, чтобы добиться этого, просто из-за некоторых ограничений в дженериках Java. Нам нужно построить наш JsonAdapter таким образом, чтобы он знал, что он десериализует общую коллекцию, а также что это за коллекция.

Moshi предлагает некоторую помощь в создании java.lang.reflect.Type, который мы можем предоставить JsonAdapter при его сборке, чтобы мы могли предоставить эту дополнительную общую информацию:

Как только это будет сделано, наш Адаптер работает точно так, как ожидалось, соблюдая эти новые универсальные ограничения:

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

7. Резюме

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

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

Мы видели, как библиотека Moshi может сделать преобразование классов Java в JSON и обратно очень простым и насколько она гибкая. Мы можем использовать эту библиотеку везде, где нам нужно конвертировать между Java и JSON — будь то загрузка и сохранение из файлов, столбцов базы данных или даже REST API. Почему бы не попробовать?

Как обычно, исходный код этой статьи можно найти на GitHub.

«