“JSR 367” alias JSON-B soll es endlich richten: ein standardisiertes JSON-Binding, ähnlich der Verarbeitung von XML mit JAXB. Gerade bei der Arbeit mit Rest-Schnittstellen und JPA stolpert man immer wieder über die gleichen Probleme. Wir machen einen ganz kurzen Rundflug über JSON-B und schauen uns eine Möglichkeit die JSON-B Adapters zu verwenden um JPA-Relationen zu mappen.
Einfache Möglichkeiten in die JSON-Generierung ein zu greifen fehlten bisher im Standard. Anbieter wie z.B. Jackson waren hier bereit viel weiter. JSON-B, Standard JSON-Binding in Java EE 8, bringt nun einige Möglichkeiten in diesem Bereich mit.
Annotationen
- @JsonbProperty, Überschreiben des JSON-Property
- @JsonbTransient, Attribute ignorieren
- @JsonbPropertyOrder, Sortierung der Attribute anpassen
- @JsonbDateFormat, Format für Datum
- @JsonbNumberFormat, Format für Zahlen
Adapter
Neben der Implementierung einer Low-Level Serialisierung (implements JsonbSerializer) gibt es die Möglichkeit einen Art Converter zu implementieren der für die Verarbeitung einzelner Attribute verwendet werden kann. Ein interessantes Szenario für diesen Fall ist z.B. Entitäten die per JPA-Relationen referenziert werden nur durch ihre IDs zu repräsentieren (anstatt durch das gesamte Objekt). Das minimiert nicht nur die zu übertragenden Daten sondern verhindert auch das Problem von “circular dependencies” bei bidirektionalen Relationen. Ein solcher (generischer) Adapter könnte so aussehen:
public abstract class EntityAdapter<T extends JPAEntity> implements JsonbAdapter<List<T>, JsonArray>{ @Inject private EntityManager em; private Class getTargetClass() { return (Class) ((ParameterizedType) getClass() .getGenericSuperclass()).getActualTypeArguments()[0]; } public JsonArray adaptToJson(List orgnl) throws Exception { JsonArrayBuilder arrayBuilder = Json.createArrayBuilder(); orgnl.stream().map(JPAEntity::getId).forEach(arrayBuilder::add); return arrayBuilder.build(); } public List adaptFromJson(JsonArray adptd) throws Exception { List list = new ArrayList(); adptd.stream().forEach(val -> { Integer id = ((JsonNumber) val).intValue(); list.add(em.getReference(getTargetClass(), id)); }); return list; } }
Das Vorgehen ist schnell erklärt. Die Liste der referenzierten Entitäten wird nur mit den IDs befüllt. Diese werden bei der Deserialisierung dazu verwendet JPA-Referenzen zu laden um die korrekten Relationen wieder her zu stellen.
Dieser Adpater kann nun in unserer Resource verwendet werden um die JSON-B Konfiguration zu erweitern
@GET public Response getTalks() { JsonbConfig config = new JsonbConfig().withAdapters(new EntityAdapter() {}); Jsonb jsonb = JsonbBuilder.create(config); List allTalks = this.talkRepository.findAll(); return Response.ok(jsonb.toJson(allTalks)).build(); }
(zu beachten ist die Verwendung der anonymen Klasse bei der Instanziierung des Adapters. Dies wird benötigt damit JSON-B den generischen Typ zur Laufzeit ermitteln zu kann)
Alternativ ließe sich ein Adapter auch fest per Annotation an der Relation definieren:
@JsonbTypeAdapter(SpeakerAdapter.class)
(stand heute gibt es hier jedoch keine Möglichkeit zusätzliche Parameter, wie den Ziel-Entity-Typ mit zu geben (im Beispiel oben durch Generics und Reflection gelöst). Demzufolge müsste hier ein konkreter Adapter pro Entity Klasse implementiert werden)
Keine Frage viele Bibliotheken zur JSON-Verarbeitung sind ein ganzes Stück weiter. Mit JSON-B geht Java EE 8 aber einen großen Schritt in die richtige Richtung.