Что такое JSON?
Нотация объектов JavaScript, или сокращенно JSON, - это формат обмена данными, который был представлен в 1999 году и получил широкое распространение в середине 2000-х годов. В настоящее время это фактически стандартный формат связи между веб-службами и их клиентами (браузерами, мобильными приложениями и т. Д.). Умение читать и писать - важный навык для любого разработчика программного обеспечения.
Несмотря на то, что JSON был получен из JavaScript, это независимый от платформы формат. Вы можете работать с ним на нескольких языках программирования, включая Java, Python, Ruby и многие другие. На самом деле любой язык, который может анализировать строку, может обрабатывать JSON.
Популярность JSON привела к его встроенной поддержке многими базами данных, последние версии PostgreSQL и MySQL содержат встроенную поддержку запросов к данным, хранящимся в полях JSON. Базы данных NoSQL, такие как MongoDB, были построены на этом формате и используют документы JSON для хранения записей, так же как таблицы и строки хранят записи в реляционной базе данных.
Одно из основных преимуществ JSON по сравнению с форматом данных XML - это размер документа. Поскольку JSON не имеет схемы, нет необходимости нести огромные структурные издержки, такие как пространства имен и оболочки.
JSON - это общий формат данных, который имеет шесть типов данных:
- Струны
- Числа
- Булевы
- Массивы
- Объекты
- ноль
Давайте посмотрим на простой документ JSON:
{
"name": "Benjamin Watson",
"age": 31,
"isMarried": true,
"hobbies": ["Football", "Swimming"],
"kids": [
{
"name": "Billy",
"age": 5
},
{
"name": "Milly",
"age": 3
}
]
}
Эта структура определяет объект, который представляет человека по имени «Бенджамин Уотсон». Здесь мы можем увидеть его данные, такие как его возраст, семейное положение и увлечения.
По сути, объект JSON - это не что иное, как строка. Строка, представляющая объект, поэтому объекты JSON часто называют строками JSON или документами JSON .
json-простой
Поскольку в Java нет встроенной поддержки JSON, прежде всего, мы должны добавить новую зависимость, которая предоставит нам ее. Для начала воспользуемся модулем json-simple , добавив его как зависимость Maven.
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>{version}</version>
</dependency>
Этот модуль полностью соответствует спецификации JSON RFC4627 и обеспечивает основные функции, такие как кодирование и декодирование объектов JSON, и не имеет никаких зависимостей от внешних модулей.
Давайте создадим простой метод, который будет принимать имя файла в качестве параметра и записывать жестко закодированные данные JSON:
public static void writeJsonSimpleDemo(String filename) throws Exception {
JSONObject sampleObject = new JSONObject();
sampleObject.put("name", "Stackabuser");
sampleObject.put("age", 35);
JSONArray messages = new JSONArray();
messages.add("Hey!");
messages.add("What's up?!");
sampleObject.put("messages", messages);
Files.write(Paths.get(filename), sampleObject.toJSONString().getBytes());
}
Здесь мы создаем экземпляр JSONObject
, добавляя имя и возраст в
качестве свойств. Затем мы создаем экземпляр класса JSONArray
складывая два строковых элемента и помещая его в качестве третьего
свойства нашего sampleObject
. В конечном итоге мы преобразуем
sampleObject
в документ JSON, вызывая метод toJSONString()
и
записывая его в файл.
Чтобы запустить этот код, мы должны создать точку входа в наше приложение, которая могла бы выглядеть так:
public class Solution {
public static void main(String[] args) throws Exception {
writeJsonSimpleDemo("example.json");
}
}
В результате выполнения этого кода мы получим файл с именем
example.json
в корне нашего пакета. Содержимое файла будет документом
JSON со всеми введенными нами свойствами:
{"name":"Stackabuser","messages":["Hey!","What's up?!"],"age":35}
Большой! У нас только что был первый опыт работы с форматом JSON, мы успешно сериализовали в него объект Java и записали его в файл.
Теперь, с небольшой модификацией нашего исходного кода, мы можем прочитать объект JSON из файла и распечатать его на консоли либо полностью, либо распечатать выбранные отдельные свойства:
public static void main(String[] args) throws Exception {
JSONObject jsonObject = (JSONObject) readJsonSimpleDemo("example.json");
System.out.println(jsonObject);
System.out.println(jsonObject.get("age"));
}
public static Object readJsonSimpleDemo(String filename) throws Exception {
FileReader reader = new FileReader(filename);
JSONParser jsonParser = new JSONParser();
return jsonParser.parse(reader);
}
Важно отметить, что метод parse()
возвращает Object
и мы должны явно
привести его к JSONObject
.
Если у вас есть искаженный или поврежденный документ JSON, вы получите исключение, подобное этому:
Exception in thread "main" Unexpected token END OF FILE at position 64.
Чтобы смоделировать это, попробуйте удалить последнюю закрывающую скобку
}
.
Копать глубже
Несмотря на то, что json-simple
полезен, он не позволяет нам
использовать пользовательские классы без написания дополнительного кода.
Предположим, у нас есть класс, представляющий человека из нашего
первоначального примера:
class Person {
Person(String name, int age, boolean isMarried, List<String> hobbies,
List<Person> kids) {
this.name = name;
this.age = age;
this.isMarried = isMarried;
this.hobbies = hobbies;
this.kids = kids;
}
Person(String name, int age) {
this(name, age, false, null, null);
}
private String name;
private Integer age;
private Boolean isMarried;
private List<String> hobbies;
private List<Person> kids;
// getters and setters
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", isMarried=" + isMarried +
", hobbies=" + hobbies +
", kids=" + kids +
'}';
}
}
Давайте возьмем документ JSON, который мы использовали в качестве
примера в начале, и поместим его в файл example.json
{
"name": "Benjamin Watson",
"age": 31,
"isMarried": true,
"hobbies": ["Football", "Swimming"],
"kids": [
{
"name": "Billy",
"age": 5
},
{
"name": "Milly",
"age": 3
}
]
}
Наша задача - десериализовать этот объект из файла в экземпляр класса
Person
Давайте сначала попробуем сделать это с помощью simple-json
Изменив наш main()
, повторно используя статический
readSimpleJsonDemo()
и добавив необходимый импорт, мы получим:
public static void main(String[] args) throws Exception {
JSONObject jsonObject = (JSONObject) readJsonSimpleDemo("example.json");
Person ben = new Person(
(String) jsonObject.get("name"),
Integer.valueOf(jsonObject.get("age").toString()),
(Boolean) jsonObject.get("isMarried"),
(List<String>) jsonObject.get("hobbies"),
(List<Person>) jsonObject.get("kids"));
System.out.println(ben);
}
Выглядит не очень хорошо, у нас много странных типов, но, похоже, он работает, не так ли?
Ну не совсем...
Попробуем вывести на консоль массив kids
Person
а затем возраст
первого ребенка.
System.out.println(ben.getKids());
System.out.println(ben.getKids().get(0).getAge());
Как мы видим, первый вывод консоли показывает, казалось бы, хороший результат:
[{"name":"Billy","age":5},{"name":"Milly","age":3}]
но второй выдает Exception
:
Exception in thread "main" java.lang.ClassCastException: org.json.simple.JSONObject cannot be cast to com.stackabuse.json.Person
Проблема здесь в том, что наше приведение типа к List<Person>
не
создавало два новых Person
, а просто заполняло то, что там было -
JSONObject
в нашем текущем случае. Когда мы попытались копнуть глубже
и узнать фактический возраст первого ребенка, мы столкнулись с
ClassCastException
.
Это большая проблема, которую, я уверен, вы сможете преодолеть, написав кучу очень умного кода, которым вы могли бы гордиться, но есть простой способ сделать это с самого начала.
Джексон
Библиотека, которая позволит нам делать все это очень эффективно, называется Jackson . Это очень распространено и используется в крупных корпоративных проектах, таких как Hibernate .
Давайте добавим это как новую зависимость Maven:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>{version}</version>
</dependency>
Базовый класс, который мы будем использовать, называется ObjectMapper
, у него есть метод readValue()
который принимает два аргумента:
источник для чтения и класс, в который будет приведен результат.
ObjectMapper
можно настроить с помощью ряда различных параметров,
передаваемых в конструктор:
FAIL_ON_SELF_REFERENCESÂ Функция, которая определяет, что происходит, когда прямая ссылка на себя обнаруживается POJO (и для него не включена обработка идентификатора объекта): либо генерируется исключение JsonMappingException (если true), либо ссылка обрабатывается нормально (false). INDENT_OUTPUT Функция, которая позволяет включать (или отключать) отступы для базового генератора, используя симпатичный принтер по умолчанию, настроенный для ObjectMapper (и ObjectWriters, созданных из mapper). ORDER_MAP_ENTRIES_BY_KEYES Функция, которая определяет, сортируются ли записи карты сначала по ключу перед сериализацией или нет: если включен, при необходимости выполняется дополнительный этап сортировки (не требуется для SortedMaps), если отключен, дополнительная сортировка не требуется. USE_EQUALITY_FOR_OBJECT_ID Функция, которая определяет, сравнивается ли идентичность объекта, используя истинную идентичность объекта на уровне JVM (false); или метод equals (). Функция, которая определяет, как сериализуется тип char []: при включении будет сериализован как явный массив JSON (с односимвольными строками в качестве значений); при отключении по умолчанию они сериализуются как строки (что более компактно). WRITE_DATE_KEYS_AS_TIMESTAMPS Функция, которая определяет, будут ли даты (и подтипы), используемые в качестве ключей карты, сериализоваться как отметки времени или нет (в противном случае будут сериализованы как текстовые значения). WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS Функция, которая контролирует, должны ли числовые значения меток времени записываться с использованием наносекундных меток времени (включено) или нет (отключено); тогда и только тогда, когда тип данных поддерживает такое разрешение. WRITE_DATES_AS_TIMESTAMPS Функция, которая определяет, должны ли значения даты (и даты / времени) (и вещи на основе даты, такие как календари) быть сериализованы как числовые отметки времени (true; по умолчанию) или как что-то еще (обычно текстовое представление). WRITE_DATES_WITH_ZONE_ID Функция, которая определяет, следует ли сериализовать значения даты / даты и времени, чтобы они включали идентификатор часового пояса, в случаях, когда сам тип содержит информацию о часовом поясе.
Полный список перечисления SerializationFeature
доступен
здесь
.
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
Person ben = objectMapper.readValue(new File("example.json"), Person.class);
System.out.println(ben);
System.out.println(ben.getKids());
System.out.println(ben.getKids().get(0).getAge());
}
К сожалению, после запуска этого фрагмента кода мы получим исключение:
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com.stackabuse.json.Person]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
Судя по всему, мы должны добавить конструктор по умолчанию в класс
Person
public Person() {}
Повторно запустив код, мы увидим еще одно исключение:
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "isMarried" (class com.stackabuse.json.Person), not marked as ignorable (5 known properties: "hobbies", "name", "married", "kids", "age"])
Это немного сложнее решить, поскольку сообщение об ошибке не сообщает нам, что делать для достижения желаемого результата. Игнорирование свойства не является жизнеспособным вариантом, поскольку мы явно имеем его в документе JSON и хотим, чтобы оно было переведено в результирующий объект Java.
Проблема здесь связана с внутренней структурой библиотеки Джексона. Он
извлекает имена свойств из геттеров, удаляя их первые части. В случае
getAge()
и getName()
он работает отлично, но с isMarried()
этого
не происходит и предполагается, что поле должно называться married
а
не isMarried
.
Жестокий, но рабочий вариант - мы можем решить эту проблему, просто
переименовав геттер в isIsMarried
. Давайте попробуем это сделать.
Больше никаких исключений не появляется, и мы видим желаемый результат!
Person{name='Benjamin Watson', age=31, isMarried=true, hobbies=[Football, Swimming], kids=[Person{name='Billy', age=5, isMarried=null, hobbies=null, kids=null}, Person{name='Milly', age=3, isMarried=null, hobbies=null, kids=null}]}
[Person{name='Billy', age=5, isMarried=null, hobbies=null, kids=null}, Person{name='Milly', age=3, isMarried=null, hobbies=null, kids=null}]
5
Хотя результат является удовлетворением, есть лучший способ обойти это ,
чем добавление другого is
для каждого из ваших логических добытчиков.
Мы можем добиться того же результата, добавив аннотацию к isMarried()
:
@JsonProperty(value="isMarried")
public boolean isMarried() {
return isMarried;
}
Таким образом, мы явно сообщаем Джексону имя поля, и ему не нужно угадывать. Это может быть особенно полезно в тех случаях, когда имя поля полностью отличается от геттеров.
Заключение
JSON - это легкий текстовый формат, который позволяет нам представлять объекты и передавать их через Интернет или хранить в базе данных.
В Java нет встроенной поддержки манипуляций с JSON, однако есть
несколько модулей, которые предоставляют эту функцию. В этом руководстве
мы рассмотрели json-simple
и Jackson
, показывая сильные и слабые
стороны каждого из них.
Работая с JSON, вы должны помнить о нюансах модулей, с которыми вы работаете, и тщательно отлаживать исключения, которые могут появляться.