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