Чтение и запись JSON на Java

Что такое JSON? Нотация объектов JavaScript, или сокращенно JSON [https://www.json.org/], представляет собой формат обмена данными, который был представлен в 1999 году и получил широкое распространение в середине 2000-х годов. В настоящее время это фактически стандартный формат связи между веб-службами и их клиентами (браузерами, мобильными приложениями и т. Д.). Умение читать и писать - важный навык для любого разработчика программного обеспечения. Несмотря на то, что JSON был получен из JavaScript, это независимый от платформы формат. Ты

Что такое 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, вы должны помнить о нюансах модулей, с которыми вы работаете, и тщательно отлаживать исключения, которые могут появляться.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus