Статьи

Генерация JSON-схемы из XSD с JAXB и Джексоном

В этой статье я продемонстрирую один из подходов к созданию схемы JSON из схемы XML (XSD). Представляя обзор подхода к созданию JSON-схемы из XML-схемы, эта статья также демонстрирует использование реализации JAXB (версия 2.2.12-b150331.1824 xjc в комплекте с JDK 9 [build 1.9.0-ea-b68]) и реализации связывания JSON / Java ( Джексон 2.5.4).

Шаги этого подхода для генерации JSON-схемы из XSD можно обобщить следующим образом:

  1. Примените компилятор JAXB xjc для генерации классов Java из XML-схемы (XSD).
  2. Примените Джексона для генерации JSON-схемы из сгенерированных JAXB Java-классов.

Генерация Java-классов из XSD с помощью JAXB’s xjc

Для целей этого обсуждения я буду использовать простой Food.xsd использованный в моем предыдущем сообщении в блоге «Нюанс JAXB: строка в сравнении с перечислением из перечисленной строки XSD с ограниченным доступом . Для удобства я воспроизвел здесь эту простую схему без комментариев XML, относящихся к предыдущему сообщению в блоге:

Food.xsd

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:dustin="http://marxsoftware.blogspot.com/foodxml"
           targetNamespace="http://marxsoftware.blogspot.com/foodxml"
           elementFormDefault="qualified"
           attributeFormDefault="unqualified">
 
   <xs:element name="Food">
      <xs:complexType>
         <xs:sequence>
            <xs:element name="Vegetable" type="dustin:Vegetable" />
            <xs:element ref="dustin:Fruit" />
            <xs:element name="Dessert" type="dustin:Dessert" />
         </xs:sequence>
      </xs:complexType>
   </xs:element>
 
   <xs:simpleType name="Vegetable">
      <xs:restriction base="xs:string">
         <xs:enumeration value="Carrot"/>
         <xs:enumeration value="Squash"/>
         <xs:enumeration value="Spinach"/>
         <xs:enumeration value="Celery"/>
      </xs:restriction>
   </xs:simpleType>
 
   <xs:element name="Fruit">
      <xs:simpleType>
         <xs:restriction base="xs:string">
            <xs:enumeration value="Watermelon"/>
            <xs:enumeration value="Apple"/>
            <xs:enumeration value="Orange"/>
            <xs:enumeration value="Grape"/>
         </xs:restriction>
      </xs:simpleType>
   </xs:element>
 
   <xs:simpleType name="Dessert">
      <xs:restriction base="xs:string">
         <xs:enumeration value="Pie"/>
         <xs:enumeration value="Cake"/>
         <xs:enumeration value="Ice Cream"/>
      </xs:restriction>
   </xs:simpleType>
 
</xs:schema>

Легко использовать инструмент командной строки xjc, предоставляемый реализацией JAXB, предоставленной JDK, для генерации классов Java, соответствующих этому XSD. Следующий снимок экрана показывает этот процесс с помощью команды:

xjc -d jaxb. \ Food.xsd

generatingJavaObjectsFromFoodXsdWithXjc

Эта простая команда генерирует классы Java, соответствующие предоставленному Food.xsd и помещает эти классы в указанный подкаталог «jaxb».

Генерация JSON из классов, сгенерированных JAXB, с Джексоном

Теперь, когда сгенерированные JAXB классы доступны, Джексон может быть применен к этим классам для генерации JSON из классов Java. На главной странице портала Джексон описан как «многоцелевая библиотека Java для обработки», которая «вдохновлена ​​качеством и разнообразием инструментов XML, доступных для платформы Java». Существование Jackson и аналогичных сред и библиотек, по-видимому, является одной из причин того, что Oracle отказалась от JEP 198 («Облегченный JSON API») с Java SE 9 . [Стоит отметить, что Java EE 7 уже имеет встроенную поддержку JSON с его реализацией JSR 353 («Java API для обработки JSON»), которая не связана с JEP 198).]

Одним из первых шагов применения Jackson для генерации JSON из наших сгенерированных JAXB Java-классов является получение и настройка экземпляра класса Jackson ObjectMapper . Один из подходов для достижения этой цели показан в следующем листинге кода.

Получение и настройка Jackson ObjectMapper для сериализации / десериализации JAXB

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * Create instance of ObjectMapper with JAXB introspector
 * and default type factory.
 *
 * @return Instance of ObjectMapper with JAXB introspector
 *    and default type factory.
 */
private ObjectMapper createJaxbObjectMapper()
{
   final ObjectMapper mapper = new ObjectMapper();
   final TypeFactory typeFactory = TypeFactory.defaultInstance();
   final AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(typeFactory);
   // make deserializer use JAXB annotations (only)
   mapper.getDeserializationConfig().with(introspector);
   // make serializer use JAXB annotations (only)
   mapper.getSerializationConfig().with(introspector);
   return mapper;
}

Приведенный выше листинг кода демонстрирует получение экземпляра Jackson ObjectMapper и его настройку на использование фабрики типов по умолчанию и интроспектора JAXB-ориентированных аннотаций.

С ObjectMapper и соответствующим образом сконфигурированным Jackson ObjectMapper экземпляр ObjectMapper легко использовать для генерации JSON из сгенерированных классов JAXB. Один из способов сделать это с помощью устаревшего класса Джексона JsonSchema продемонстрирован в следующем листинге кода.

Генерация JSON из классов Java с устаревшим классом com.fasterxml.jackson.databind.jsonschema.JsonSchema

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
 * Write JSON Schema to standard output based upon Java source
 * code in class whose fully qualified package and class name
 * have been provided.
 *
 * @param mapper Instance of ObjectMapper from which to
 *     invoke JSON schema generation.
 * @param fullyQualifiedClassName Name of Java class upon
 *    which JSON Schema will be extracted.
 */
private void writeToStandardOutputWithDeprecatedJsonSchema(
   final ObjectMapper mapper, final String fullyQualifiedClassName)
{
   try
   {
      final JsonSchema jsonSchema = mapper.generateJsonSchema(Class.forName(fullyQualifiedClassName));
      out.println(jsonSchema);
   }
   catch (ClassNotFoundException cnfEx)
   {
      err.println("Unable to find class " + fullyQualifiedClassName);
   }
   catch (JsonMappingException jsonEx)
   {
      err.println("Unable to map JSON: " + jsonEx);
   }
}

Код в приведенном выше листинге создает экземпляр класса для предоставленного Java-класса (класс Food высшего уровня, сгенерированный компилятором JAXB xjc в моем примере) и передает эту ссылку на класс, сгенерированный JAXB, в generateJaSerma generateJsonSchema (Class < ?>) метод. Устаревшая реализация класса toString () класса JsonSchema очень полезна и позволяет легко выписывать JSON, сгенерированный из классов, сгенерированных JAXB.

Для целей этой демонстрации я предоставляю драйвер демонстрации в качестве основной (String []) функции. Эта функция и весь класс до этого момента (включая методы, показанные выше) представлены в следующем листинге кода.

JsonGenerationFromJaxbClasses.java, версия 1

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package dustin.examples.jackson;
 
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
 
import com.fasterxml.jackson.databind.jsonschema.JsonSchema;
 
import static java.lang.System.out;
import static java.lang.System.err;
 
/**
 * Generates JavaScript Object Notation (JSON) from Java classes
 * with Java API for XML Binding (JAXB) annotations.
 */
public class JsonGenerationFromJaxbClasses
{
   /**
    * Create instance of ObjectMapper with JAXB introspector
    * and default type factory.
    *
    * @return Instance of ObjectMapper with JAXB introspector
    *    and default type factory.
    */
   private ObjectMapper createJaxbObjectMapper()
   {
      final ObjectMapper mapper = new ObjectMapper();
      final TypeFactory typeFactory = TypeFactory.defaultInstance();
      final AnnotationIntrospector introspector = new JaxbAnnotationIntrospector(typeFactory);
      // make deserializer use JAXB annotations (only)
      mapper.getDeserializationConfig().with(introspector);
      // make serializer use JAXB annotations (only)
      mapper.getSerializationConfig().with(introspector);
      return mapper;
   }
 
   /**
    * Write out JSON Schema based upon Java source code in
    * class whose fully qualified package and class name have
    * been provided.
    *
    * @param mapper Instance of ObjectMapper from which to
    *     invoke JSON schema generation.
    * @param fullyQualifiedClassName Name of Java class upon
    *    which JSON Schema will be extracted.
    */
   private void writeToStandardOutputWithDeprecatedJsonSchema(
      final ObjectMapper mapper, final String fullyQualifiedClassName)
   {
      try
      {
         final JsonSchema jsonSchema = mapper.generateJsonSchema(Class.forName(fullyQualifiedClassName));
         out.println(jsonSchema);
      }
      catch (ClassNotFoundException cnfEx)
      {
         err.println("Unable to find class " + fullyQualifiedClassName);
      }
      catch (JsonMappingException jsonEx)
      {
         err.println("Unable to map JSON: " + jsonEx);
      }
   }
 
   /**
    * Accepts the fully qualified (full package) name of a
    * Java class with JAXB annotations that will be used to
    * generate a JSON schema.
    *
    * @param arguments One argument expected: fully qualified
    *     package and class name of Java class with JAXB
    *     annotations.
    */
   public static void main(final String[] arguments)
   {
      if (arguments.length < 1)
      {
         err.println("Need to provide the fully qualified name of the highest-level Java class with JAXB annotations.");
         System.exit(-1);
      }
      final JsonGenerationFromJaxbClasses instance = new JsonGenerationFromJaxbClasses();
      final String fullyQualifiedClassName = arguments[0];
      final ObjectMapper objectMapper = instance.createJaxbObjectMapper();
      instance.writeToStandardOutputWithDeprecatedJsonSchema(objectMapper, fullyQualifiedClassName);
   }
}

Чтобы запустить этот относительно общий код для классов Java, сгенерированных xjc JAXB на основе Food.xsd , мне нужно предоставить полное имя пакета и имя класса сгенерированного класса самого высокого уровня. В данном случае это com.blogspot.marxsoftware.foodxml.Food (имя пакета основано на пространстве имен XSD, поскольку я явно не переопределял его при запуске xjc ). Когда я запускаю приведенный выше код с этим полностью определенным именем класса и с классами JAXB и библиотеками Джексона на пути к классам, я вижу следующий JSON, записанный в стандартный вывод.

Сгенерированный JSON

1
{"type":"object","properties":{"vegetable":{"type":"string","enum":["CARROT","SQUASH","SPINACH","CELERY"]},"fruit":{"type":"string"},"dessert":{"type":"string","enum":["PIE","CAKE","ICE_CREAM"]}}}

Люди (включая многих разработчиков) предпочитают более красивую печать, чем то, что было показано для созданного JSON. Мы можем настроить реализацию метода демонстрационного класса writeToStandardOutputWithDeprecatedJsonSchema(ObjectMapper, String) как показано ниже, чтобы выписать JSON с отступом, который лучше отражает его иерархическую природу. Этот модифицированный метод показан ниже.

Модифицированный writeToStandardOutputWithDeprecatedJsonSchema (ObjectMapper, String) для записи с отступом JSON

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * Write out indented JSON Schema based upon Java source
 * code in class whose fully qualified package and class
 * name have been provided.
 *
 * @param mapper Instance of ObjectMapper from which to
 *     invoke JSON schema generation.
 * @param fullyQualifiedClassName Name of Java class upon
 *    which JSON Schema will be extracted.
 */
private void writeToStandardOutputWithDeprecatedJsonSchema(
   final ObjectMapper mapper, final String fullyQualifiedClassName)
{
   try
   {
      final JsonSchema jsonSchema = mapper.generateJsonSchema(Class.forName(fullyQualifiedClassName));
      out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema));
   }
   catch (ClassNotFoundException cnfEx)
   {
      err.println("Unable to find class " + fullyQualifiedClassName);
   }
   catch (JsonMappingException jsonEx)
   {
      err.println("Unable to map JSON: " + jsonEx);
   }
   catch (JsonProcessingException jsonEx)
   {
      err.println("Unable to process JSON: " + jsonEx);
   }
}

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

Сгенерированный JSON с иерархией связи отступов

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
{
  "type" : "object",
  "properties" : {
    "vegetable" : {
      "type" : "string",
      "enum" : [ "CARROT", "SQUASH", "SPINACH", "CELERY" ]
    },
    "fruit" : {
      "type" : "string"
    },
    "dessert" : {
      "type" : "string",
      "enum" : [ "PIE", "CAKE", "ICE_CREAM" ]
    }
  }
}

Я использовал Джексон 2.5.4 в этом посте. Класс com.fasterxml.jackson. databind .jsonschema.JsonSchema com.fasterxml.jackson. databind .jsonschema.JsonSchema устарела в этой версии с комментарием: «Начиная с версии 2.2, мы рекомендуем использовать внешний модуль генератора JSON Schema» . Учитывая это, я теперь рассмотрим использование нового предпочтительного подхода (подход к схеме JSON JSON ).

Наиболее значительным изменением является использование класса JsonSchema в пакете com.fasterxml.jackson.module.jsonSchema , а не использование класса JsonSchema в пакете com.fasterxml.jackson.databind.jsonschema . Подходы для получения экземпляров этих различных версий классов JsonSchema также различны. Следующий листинг кода демонстрирует использование более нового предпочтительного подхода для генерации JSON из классов Java.

Использование Джексона новее и предпочтительнее com.fasterxml.jackson.module.jsonSchema.JsonSchema

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * Write out JSON Schema based upon Java source code in
 * class whose fully qualified package and class name have
 * been provided. This method uses the newer module JsonSchema
 * class that replaces the deprecated databind JsonSchema.
 *
 * @param fullyQualifiedClassName Name of Java class upon
 *    which JSON Schema will be extracted.
 */
private void writeToStandardOutputWithModuleJsonSchema(
   final String fullyQualifiedClassName)
{
   final SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
   final ObjectMapper mapper = new ObjectMapper();
   try
   {
      mapper.acceptJsonFormatVisitor(mapper.constructType(Class.forName(fullyQualifiedClassName)), visitor);
      final com.fasterxml.jackson.module.jsonSchema.JsonSchema jsonSchema = visitor.finalSchema();
      out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema));
   }
   catch (ClassNotFoundException cnfEx)
   {
      err.println("Unable to find class " + fullyQualifiedClassName);
   }
   catch (JsonMappingException jsonEx)
   {
      err.println("Unable to map JSON: " + jsonEx);
   }
   catch (JsonProcessingException jsonEx)
   {
      err.println("Unable to process JSON: " + jsonEx);
   }
}

В следующей таблице сравнивается использование двух классов Jackson JsonSchema бок о бок с устаревшим подходом, показанным ранее слева (немного адаптированный для этого сравнения), и рекомендуемым более новым подходом справа. Оба генерируют один и тот же вывод для одного и того же данного Java-класса, из которого должен быть написан JSON.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * Write out JSON Schema based upon Java source code in
 * class whose fully qualified package and class name have
 * been provided. This method uses the deprecated JsonSchema
 * class in the "databind.jsonschema" package
 * {@see com.fasterxml.jackson.databind.jsonschema}.
 *
 * @param fullyQualifiedClassName Name of Java class upon
 *    which JSON Schema will be extracted.
 */
private void writeToStandardOutputWithDeprecatedDatabindJsonSchema(
   final String fullyQualifiedClassName)
{
   final ObjectMapper mapper = new ObjectMapper();
   try
   {
      final com.fasterxml.jackson.databind.jsonschema.JsonSchema jsonSchema =
         mapper.generateJsonSchema(Class.forName(fullyQualifiedClassName));
      out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema));
   }
   catch (ClassNotFoundException cnfEx)
   {
      err.println("Unable to find class " + fullyQualifiedClassName);
   }
   catch (JsonMappingException jsonEx)
   {
      err.println("Unable to map JSON: " + jsonEx);
   }
   catch (JsonProcessingException jsonEx)
   {
      err.println("Unable to process JSON: " + jsonEx);
   }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * Write out JSON Schema based upon Java source code in
 * class whose fully qualified package and class name have
 * been provided. This method uses the newer module JsonSchema
 * class that replaces the deprecated databind JsonSchema.
 *
 * @param fullyQualifiedClassName Name of Java class upon
 *    which JSON Schema will be extracted.
 */
private void writeToStandardOutputWithModuleJsonSchema(
   final String fullyQualifiedClassName)
{
   final SchemaFactoryWrapper visitor = new SchemaFactoryWrapper();
   final ObjectMapper mapper = new ObjectMapper();
   try
   {
      mapper.acceptJsonFormatVisitor(mapper.constructType(Class.forName(fullyQualifiedClassName)), visitor);
      final com.fasterxml.jackson.module.jsonSchema.JsonSchema jsonSchema = visitor.finalSchema();
      out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonSchema));
   }
   catch (ClassNotFoundException cnfEx)
   {
      err.println("Unable to find class " + fullyQualifiedClassName);
   }
   catch (JsonMappingException jsonEx)
   {
      err.println("Unable to map JSON: " + jsonEx);
   }
   catch (JsonProcessingException jsonEx)
   {
      err.println("Unable to process JSON: " + jsonEx);
   }
}

В этом посте блога показаны два подхода, использующие разные версии классов с именем JsonSchema предоставленные Джексоном для написания JSON на основе классов Java, сгенерированных из XSD с помощью xjc JAXB. Общий процесс, продемонстрированный в этом посте, представляет собой один из подходов к созданию JSON-схемы из XML-схемы.