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 и обратно точно так, как ожидалось. Это охватывает:
- Все примитивы —
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
, прежде чем они будут использоваться. Мы скоро увидим этот точный шаблон, когда добавим поддержку наших собственных пользовательских типов:
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 .