REST: как сериализовать java-объект JSON в "мелкой" форме?
Предположим, что у меня есть следующие сущности JPA:
@Entity
public class Inner {
@Id private Long id;
private String name;
// getters/setters
}
@Entity
public class Outer {
@Id private Long id;
private String name;
@ManyToOne private Inner inner;
// getters/setters
}
Оба Spring и java EE имеют реализации REST с сериализаторами по умолчанию, которые будут маршировать объекты в/из JSON без дальнейшего кодирования. Но при преобразовании Outer
в JSON оба Spring и EE вставляют в него полную копию Inner
:
// Outer
{
"id": "1234",
"name": "MyOuterName",
"inner": {
"id": "4321",
"name": "MyInnerName"
}
}
Это правильное поведение, но проблематично для моих веб-сервисов, поскольку графы объектов могут быть глубокими/сложными и содержать круговые ссылки. Есть ли способ настроить поставляемый маршаллер для маршаллирования POJO/сущностей "мелким" способом вместо того, чтобы не создавать пользовательский сериализатор JSON для каждого из них? Один пользовательский сериализатор, который работает на всех объектах, будет в порядке. В идеале мне бы хотелось что-то вроде этого:
// Outer
{
"id": "1234",
"name": "MyOuterName",
"innerId": "4321"
}
Я также хотел бы, чтобы "unmarshall" JSON вернулся в эквивалентный Java-объект. Бонусы, если решение работает как с Spring, так и с java EE. Спасибо!
Ответы
Ответ 1
Для разборки сложных графиков объектов с использованием jaxb @XmlID
и @XmlIDREF
выполняется для.
public class JSONTestCase {
@XmlRootElement
public static final class Entity {
private String id;
private String someInfo;
private DetailEntity detail;
@XmlIDREF
private DetailEntity detailAgain;
public Entity(String id, String someInfo, DetailEntity detail) {
this.id = id;
this.someInfo = someInfo;
this.detail = detail;
this.detailAgain = detail;
}
// default constructor, getters, setters
}
public static final class DetailEntity {
@XmlID
private String id;
private String someDetailInfo;
// constructors, getters, setters
}
@Test
public void testMarshalling() throws JAXBException {
Entity e = new Entity( "42", "info", new DetailEntity("47","detailInfo") );
JAXBContext context = org.eclipse.persistence.jaxb.JAXBContextFactory.createContext(new Class[]{Entity.class}, null);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
m.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
m.marshal(e, System.out);
}
}
Это приведет к следующему фрагменту json-фрагмента
{
"detailAgain" : "47",
"detail" : {
"id" : "47",
"someDetailInfo" : "detailInfo"
},
"id" : "42",
"someInfo" : "info"
}
Unmarshalling этого json гарантирует, что detail
и detailAgain
являются одинаковыми экземплярами.
Две аннотации являются частью jaxb, поэтому она будет работать в Spring, а также в java EE. Маршаллинг для json не является частью стандарта, поэтому я использую moxy в примере.
Обновление
Явно использование moxy не обязательно в ресурсе JAX-RS. Следующие ярлыки отлично работают на контейнере java-EE-7 (glassfish 4.1.1) и приводят к вышеупомянутому фрагменту json:
@Stateless
@Path("/entities")
public class EntityResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Entity getEntity() {
return new Entity( "42", "info", new DetailEntity("47","detailInfo") );
}
}
Ответ 2
После многих проблем я объясняю Кассио Маццочи Молину, что "использование сущностей в вашем REST API не может быть хорошей идеей"
Я бы сделал, чтобы бизнес-уровень преобразовывал объекты сущности в DTO.
Вы можете сделать это очень легко с помощью библиотек, таких как mapstruct
Если вы все еще хотите продолжить эту плохую практику, вы можете использовать jackson и настроить ваш джектор-картограф
Ответ 3
У меня была такая же проблема, и в результате я использовал аннотации Jackson для своих сущностей для управления сериализацией:
Что вам нужно, это @JsonIdentityReference (alwaysAsId = true), чтобы проинструктировать сериализатор bean, что эта ссылка должна быть только идентификатором. Вы можете увидеть пример моего репо:
https://github.com/sashokbg/company-rest-service/blob/master/src/main/java/bg/alexander/model/Order.java
@OneToMany(mappedBy="order", fetch=FetchType.EAGER)
@JsonIdentityReference(alwaysAsId=true) // otherwise first ref as POJO, others as id
private Set<OrderDetail> orderDetails;
Если вы хотите полностью контролировать, как ваши объекты представлены как JSON, вы можете использовать JsonView для определения того, какое поле сериализовано для вашего представления.
@JsonView (Views.Public.class) public int id;
@JsonView(Views.Public.class)
public String itemName;
@JsonView(Views.Internal.class)
public String ownerName;
http://www.baeldung.com/jackson-json-view-annotation
Приветствия!
Ответ 4
для этой проблемы Существует два решения.
1 - использование вида Jackson Json
2- Создание двух классов сопоставления для внутренней структуры. один из них включает пользовательские поля, а другой включает все поля...
Я думаю, что взгляд Jackson Json - лучшее решение...
Ответ 5
Пройдите через библиотеку FLEXJSON, чтобы включить/исключить иерархию вложенных классов при сериализации объектов Java.
Примеры для flexjson.JSONSerializer
представлены здесь
Ответ 6
Вы можете отсоединить объект JPA до сериализации, если вы используете lazyloading, чтобы не загружать вспомогательные объекты.
Другой способ, но зависит от API-интерфейса последовательного интерфейса JSON, вы можете использовать аннотацию "переходный" или специфический.
Почему JPA имеет аннотацию @Transient?
Плохой способ - использовать инструмент, например dozer, для копирования объекта JPA в другом классе, только для свойств, необходимых для json (но он работает... немного накладных расходов на память, процессор и время...)
@Entity
public class Outer {
@Id private Long id;
private String name;
@ManyToOne private Inner inner;
//load manually inner.id
private final Long innerId;
// getters/setters
}