Перейти к основному содержимому

Знакомство с Моши Джсоном

· 11 мин. чтения

1. Введение

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

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

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

Прежде чем мы сможем его использовать, нам сначала нужно добавить зависимости Moshi JSON в наш файл 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", "ForEach", "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"ForEach","text":"This is my post","title":"My Post"}

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

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

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

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

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

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

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

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

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

5. Пользовательские типы в Moshi

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Post post = new Post("My Post", new Author("ForEach", "foreach@example.com"), "This is my post");
String json = jsonAdapter.toJson(post);
// {"author":"ForEach <foreach@example.com>","text":"This is my post","title":"My Post"}

Post post = jsonAdapter.fromJson(json);
// new Post("My Post", new Author("ForEach", "foreach@example.com"), "This is my post");

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

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

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

Используя Moshi, все, что нам нужно сделать, это написать тип Java, представляющий желаемый результат, а затем наш метод @ToJson может вернуть этот новый объект Java, который Moshi затем преобразует в 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);
}
}

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

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

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

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. Адаптеры альтернативного типа

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

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

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

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

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

Далее нам нужно написать адаптер. Как всегда, у нас есть метод @FromJson и @ToJson для преобразования между нашим типом и 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);
}
}

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

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

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

Теперь мы можем преобразовать наш аннотированный тип в 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", "ForEach", Instant.now()));
// {"author":"ForEach","posted":1582095384793,"title":"Introduction to Moshi Json"}

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

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

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

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

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

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

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

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

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

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

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

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

6.2. Переходные поля

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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. Разбор массивов JSON

Все, что мы делали до сих пор, предполагало, что мы сериализуем и десериализуем один объект 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);

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

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

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

7. Резюме

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

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