Генерируем бины по json описанию

… и перестаём наконец вручную описывать служебные классы

Спойлер

Мне частенько приходится работать с каким-то апи, которое понимает, либо отдает json. Давно научившись сериализовать/десериализовать объектики при наличии бинов и библиотечки Gson в одну строку, сами бины муторно до сих пор писались вручную. И тут, снова взглянув на xsd схемки, по которым рядом генерировались объектики из xml, мне стало дико жалко времени на описание объекта самостоятельно. И я решил попробовать свежий плагин для любимого maven.

Как это было раньше

Наверняка найдутся те, кто раньше парсил xml или json схожим образом:

protected SomeObj parseFileById(String id) throws Exception {
   if (id.contains("\'")) {
       id = escapeXPath(id);
   }
   String name = parseAndReturnSingleNodeValue("//file[id='" + id + "']/name/text()");
   SomeObj file = new SomeObj(name, id);
   file.setMd5(parseAndReturnSingleNodeValue("//file[id='" + id + "']/meta/md5/text()"));
   file.setHash(parseAndReturnSingleNodeValue("//file[id='" + id + "']/meta/public_hash/text()"));
   file.setType(parseAndReturnSingleNodeValue("//file[id='" + id + "']/type/text()"));
   file.setSize(parseAndReturnSingleNodeValue("//file[id='" + id + "']/size/text()"));
   return file;
}

Выглядит жутко - смесь XPath, строк, куча сеттеров, какие-то предпроверки… А вдруг еще и выдача изменится! Такой код будет содержать в актуальном состоянии нелегко. Как это зарефакторить для xml - понятно. Но как быть, если нужен только json (или вы не умеете писать xsd схемы)?

jsonschema2pojo

Есть замечательный проект - http://www.jsonschema2pojo.org. Функциональная часть его онлайновой составляющей может быть полностью использована в качестве мавен плагина.

Для справки - POJO = Plain Old Java Objects

Кратко что делаем

Подключаем плагин в секции build.plugins нашего помника:

<!--http://joelittlejohn.github.io/jsonschema2pojo/site/0.4.1/generate-mojo.html-->
<plugin>
   <groupId>org.jsonschema2pojo</groupId>
   <artifactId>jsonschema2pojo-maven-plugin</artifactId>
   <version>0.4.1</version>
   <configuration>
       <sourceDirectory>src/main/resources/json</sourceDirectory>
       <outputDirectory>${project.build.directory}/generated-sources/java-gen</outputDirectory>
       <targetPackage>ru.qatools.json2pojo.beans</targetPackage>
       <annotationStyle>gson</annotationStyle>
       <sourceType>json</sourceType>
       <generateBuilders>true</generateBuilders>
   </configuration>
   <executions>
       <execution>
          <goals>
             <goal>generate</goal>
          </goals>
       </execution>
   </executions>
</plugin>
<!--//-->

Кладем в ресурсах, в папочке json наши файлы (всё как указали в секции sourceDirectory):

  • bounce-bean.json
{
    "bounce": {
        "final-recipient": "<email>",
        "status": "<code>",
        "type": "failed"
    }
}

Далее, делаем mvn clean compile, и радуемся.

img-ready-to-use

Подробности по настройкам

Все настройки отлично документированы, поэтому за полным набором стоит обращаться на страницу проекта, в раздел Documentation for the Maven plugin.

Что использовал я:

  • sourceDirectory - тут кажется все ясно - где найти оригиналы.
  • outputDirectory - эту настройку можно не переопределять, тогда классы окажутся в папочке на уровень выше. Но я держу рядом бины по xsd схемам, поэтому пусть будут вместе, в generated-sources
  • targetPackage - тут тоже несложно - в каком пакете окажутся классы. На скриншоте видно, что папки внутри оригинальной директории добавляют пакет к указанному.
  • annotationStyle - в этой секции указывается какого рода аннотации проставятся над полями. Я использую Gson для маршаллинга, поэтому указал gson. Можно указать none - тогда никаких аннотаций не будет. Можно указать так же jackson. Хочу обратить внимание на возможность кастомных аннотаторов.
  • sourceType - в этом примере у нас используется json. Вообще рекомендую ознакомиться с jsonscheme, тогда можно более гибко настраивать генерируемые объекты (как с xsd)
  • generateBuilders - очень полезная возможность сгенерировать сеттеры, возвращающие сам объект. Если планируется менять объект вручную, то просто mustbetrue!
  new BounceBean().withBounce(
      new Bounce().withFinalRecipient("blablauser@ya.ru")
                  .withStatus("403")
                  .withType("failed")
  )

Пример продвинутого использования

Предположим, к вам приходит json-массив, где каждый объект - это описание определенного файла. И нам нужно из всего списка выбрать только файлы с заданной строкой в имени.

public List<ResourceFile> files(String contains) {
   List<ResourceFile> resourceFiles = new Gson().fromJson(resp().asString(), new TypeToken<List<ResourceFile>>() {
   }.getType());
   return with(resourceFiles).retain(having(on(ResourceFile.class).getName(), containsString(contains)));
}

Пользуясь технологией выше, мы копипастим пример json для конкретного файла и генерируем бин. После чего десериализуем в List<ResourceFile> весь пришедший json-массив, используя new TypeToken<List<ResourceFile>>().

А вот дальше - самое интересное. Подключаем lambdaj (если до сих пор еще этого не сделали!):

<dependency>
	<groupId>com.googlecode.lambdaj</groupId>
	<artifactId>lambdaj</artifactId>
	<version>2.3.3</version>
</dependency>

…и забываем о том что Java многословна.

Можно это дело приправить фильтрацией немножко с другой стороны, при помощи JsonPath:

public List<ResourceFile> files(Criteria... criterias) {
   List<String> filesJson = read(resp().asString(), "$.[?]",
      filter(where("type").eq("file").andOperator(criterias)));
   return new Gson().fromJson(filesJson.toString(), new TypeToken<List<ResourceFile>>() {
   }.getType());
}


public static Criteria byName(String name) {
   return Criteria.where("name").eq(name);
}

получается не всегда так же гибко, как с lambdaj, но иногда удобнее.