Настройка json сериализации сущностей для приема на стороне spring data rest

Итак, вам удалось настроить свой сервер со spring data rest и hibernate. Теперь надо понять, какие запросы и какие JSON сервер ждет для создания и изменения сущностей в БД. Для примера возьмем сущности абстрактного форума: тема Topic, ее статус Status, пользователь User, список поисковых тегов List<Tag>.

У каждой темы есть связь one-to-many к Post, т.е. у нее есть список ответов. Со статусом будет связь many-to-one. С пользователем будет связь many-to-one. C тегами связь many-to-many(для простоты, однонаправленная — только Topic знает список своих тегов). У каждой сущности есть id.

Теперь нужно понять, что от нас ждет сервер. Запросы я проверял в RestClient для FireFox (не забудьте выставить заголовок Content-Type: application/json).

Для простых сущностей тело запроса на создание такое (сущность Tag):

{
"text" : "rest"
}

Его надо послать в POST запросе на end point http://localhost:8080/server/tags . Список полей, разумеется, может быть расширен. Главное, чтоб имена совпадали с теми, которые ожидает сервер. Если вы не навешивали аннотации из jackson, то имена сопадают с именами полей сущности. В случае успеха в ответ придет ответ из семейства двухсотых, а в теле будет сериализованная созданная сущность. Из нее нам пригодится присвоенный id.
Аналогично создаются сущности Status, User.

А вот на сущности Topic надо задержаться. Для нее запрос будет таким:

{
"createDate" : "2016-10-11",
"status" : "http://localhost:8080/server/statuses/10",
"author" : "http://localhost:8080/server/author/3",
"tags" : ["http://localhost:8080/server/tags/1", "http://localhost:8080/server/tags/2"]
}

Это снова POST запрос, при передаче списка сущностей для связи ManyToMany контент тип (content-type) должен быть text/uri-list. Вложенные сущности, которые мы используем по ссылке, уже должны быть созданы.
Для создания темы без тегов надо передать «tags» : [].

Если вам нужно изменить уже существующую сущность, то к вашим услугам запрос PATCH:

PATCH http://localhost:8080/server/tags/11
{
"text" : "json"
}

Пересылаем только те поля, которые хотим изменить. В случае успеха придет опять-таки 2хх ответ и представление измененной сущности. Будьте оснорожны с массивами — если пошлете пустой, то он и сохранится в БД. Id в теле JSON включать не нужно, он берется из адреса запроса (в данном случае = 11).

Теперь по поводу сериализации на стороне клиента, если уж мы знаем, что от нас ждет сервер. Клиент построен на vaadin и spring на нем в виде зависимосей vaadin-spring spring-plugin-core и spring-hateoas.

Если не писать свой сериализатор, то поведение по умолчанию для сложных сущностей будет генерировать представление в виде JSON вроде такого:

{"active":null,"createDate":[2016,10,11],"topicStatus":{"active":true,"status":"[0xd0][0x9e][0xd1][0x82][0xd0][0xbc][0xd0][0xb5][0xd0][0xbd][0xd0][0xb5][0xd0][0xbd][0xd0][0xbe]","links":[{"rel":"self","href":"http://localhost:8080/server/api/topicStatuses/2"},{"rel":"topicStatus","href":"http://localhost:8080/server/api/topicStatuses/2"}],"id":2},"fullName":null,"member":{"active":true,"login":"tvegorov","name":"[0xd0][0x9f][0xd0][0xb5][0xd1][0x82][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0] [0xd0][0xa2][0xd0][0xb0][0xd1][0x82][0xd1][0x8c][0xd1][0x8f][0xd0][0xbd][0xd0][0xb0] [0xd0][0x98][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd\0xd0][0xbe][0xd0][0xb2][0xd0][0xbd][0xd0][0xb0]","role":{"active":true,"role":"[0xd0][0x9e][0xd0][0xbf][0xd0][0xb5][0xd1][0x80][0xd0][0xb0][0xd1][0x82][0xd0][0xbe][0xd1][0x80] [0xd0][0x9e][0xd0][0x9f][0xd0][0xa1]","links":[],"id":1},"creator":{"active":true,"name":"[0xd0][0x92][0xd0][0x9e][0xd0][0xa7][0xd0][0x95][0xd0][0x9f][0xd0][0xa8][0xd0][0x98][0xd0][0x99]","index":"385274","address":"[0xd1][0x80][0xd0][0xb5][0xd1][0x81][0xd0][0xbf]. [0xd0][0x90][0xd0][0xb4][0xd1][0x8b][0xd0][0xb3][0xd0][0xb5][0xd1][0x8f] [0xd1][0x80]-[0xd0][0xbd] [0xd0][0xa2][0xd0][0xb5][0xd1][0x83][0xd1][0x87][0xd0][0xb5][0xd0][0xb6][0xd1][0x81][0xd0][0xba][0xd0][0xb8][0xd0][0xb9] [0xd0][0xb0][0xd1][0x83][0xd0][0xbb] [0xd0][0x92][0xd0][0xbe][0xd1][0x87][0xd0][0xb5][0xd0][0xbf][0xd1][0x88][0xd0][0xb8][0xd0][0xb9] [0xd1][0x83][0xd0][0xbb]. [0xd0][0x93][0xd0][0xb0][0xd0][0xb3][0xd0][0xb0][0xd1][0x80][0xd0][0xb8][0xd0][0xbd][0xd0][0xb0] [0xd0][0xb4][0xd0][0xbe][0xd0][0xbc] 3","tags":[{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 129","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/19"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/19{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/19/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/19/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/19/status"}],"id":19},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 125","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/15"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/15{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/15/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/15/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/15/status"}],"id":15},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 121","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0xa1][0xd0][0xb2][0xd0][0xbe][0xd0][0xb1][0xd0][0xbe][0xd0][0xb4][0xd0][0xb5][0xd0][0xbd]","links":[],"id":1},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/11"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/11{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/11/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/11/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/11/status"}],"id":11},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 130","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/20"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/20{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/20/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/20/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/20/status"}],"id":20},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 126","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/16"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/16{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/16/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/16/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/16/status"}],"id":16},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 124","size":{"active":true,"size":"[0xd0][0x9c][0xd0][0xb0][0xd0][0xbb][0xd0][0xb5][0xd0][0xbd][0xd1][0x8c][0xd0][0xba][0xd0][0xb0][0xd1][0x8f]","links":[],"id":1},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/14"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/14{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/14/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/14/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/14/status"}],"id":14},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 122","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbd][0xd0][0xb5] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":2},"releaseDate":null,"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/12"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/12{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/12/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/12/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/12/status"}],"id":12},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 128","size":{"active":true,"size":"[0xd0][0x91][0xd0][0xbe][0xd0][0xbb][0xd1][0x8c][0xd1][0x88][0xd0][0xb0][0xd1][0x8f]","links":[],"id":3},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,11,1],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/18"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/18{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/18/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/18/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/18/status"}],"id":18},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 127","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,12,11],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/17"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/17{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/17/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/17/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/17/status"}],"id":17},{"active":true,"number":"[0xd0][0x90][0xd0][0xaf] 123","size":{"active":true,"size":"[0xd0][0xa1][0xd1][0x80][0xd0][0xb5][0xd0][0xb4][0xd0][0xbd][0xd1][0x8f][0xd1][0x8f]","links":[],"id":2},"status":{"active":true,"status":"[0xd0][0x97][0xd0][0xb0][0xd1][0x80][0xd0][0xb5][0xd0][0xb7][0xd0][0xb5][0xd1][0x80][0xd0][0xb2][0xd0][0xb8][0xd1][0x80][0xd0][0xbe][0xd0][0xb2][0xd0][0xb0][0xd0][0xbd] [0xd0][0xb8] [0xd0][0xbe][0xd0][0xbf][0xd0][0xbb][0xd0][0xb0][0xd1][0x87][0xd0][0xb5][0xd0][0xbd]","links":[],"id":3},"releaseDate":[2016,12,10],"links":[{"rel":"self","href":"http://localhost:8080/server/api/tags/13"},{"rel":"tag","href":"http://localhost:8080/server/api/tags/13{?projection}"},{"rel":"size","href":"http://localhost:8080/server/api/tags/13/size"},{"rel":"creator","href":"http://localhost:8080/server/api/tags/13/creator"},{"rel":"status","href":"http://localhost:8080/server/api/tags/13/status"}],"id":13}],"links":[{"rel":"self","href":"http://localhost:8080/server/api/creators/2"},{"rel":"creator","href":"http://localhost:8080/server/api/creators/2"},{"rel":"tagList","href":"http://localhost:8080/server/api/creators/2/tagList"}],"id":2},"links":[{"rel":"self","href":"http://localhost:8080/server/api/members/2"},{"rel":"member","href":"http://localhost:8080/server/api/members/2{?projection}"},{"rel":"role","href":"http://localhost:8080/server/api/members/2/role"},{"rel":"creator","href":"http://localhost:8080/server/api/members/2/creator"}],"id":2},"quantity":1,"client":null,"cost":100,"tags":null,"links":[],"id":null}

Взято из отладочного логгирования спринга, в квадратных скобках шестнадцатиричное представление русских букв.
Вложенность и количество ненужных данных можно посмотреть, отформатировав JSON тут: https://jsonformatter.curiousconcept.com/
Как видно из концовки, это тело запроса на создание («id»:null).
Из-за такого нагромождения сервер вряд ли сможет создать сущность — создатели spring data rest не предусмотрели обработку таких монстров. Я сталкивался с тем, что подобный JSON с воспринимался как запрос на редактирование существующей сущности — id извлекался из одной из вложенных сущностей. Хорошо, что у нее была оптимистическая блокировка с @Version, которая генерила ошибку.

Поэтому надо писать для сложных сущностей сериализатор и подключать его.
Пример:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
 
public class TopicSerializer extends JsonSerializer<topic> {
 
    @Override
    public void serialize(Topic topic, JsonGenerator generator, SerializerProvider provider) throws IOException {
        generator.writeStartObject();
 
	if (topic.getStatus() != null) {
            generator.writeStringField("status", topic.getStatus().getId().getHref());
        }
 
	if (topic.getAuthor() != null) {
            generator.writeStringField("status", topic.getAuthor().getId().getHref());
        }
 
	List<tag> list = topic.getTags();
        if (list != null) {
            generator.writeFieldName("tags");
            generator.writeStartArray(); // [
            if (list.size() &gt; 0) {
                for (Tag tag : list) {
                    generator.writeString(tag.getId().getHref());
                }
            }
            generator.writeEndArray(); // ]
        }
        generator.writeEndObject();
	}
}
</tag></topic>

Примечание 1. На стороне клиента классы с описанием сущностей наследуются от org.springframework.hateoas.ResourceSupport, что и позволяет работать в них со ссылками.
Примечание 2. Если вы хотите позволить передачу пустой строки (или пустого списка, как в примере) в JSON, то проверка должна быть, такой, чтоб отсекать только null:

if (topic.getString() != null) {
    generator.writeStringField("string", topic.getString());
}

В случае, если нужно отсекать не только null, но и пустые строки и строки только из white space, можно сделать так:

import org.apache.commons.lang3.StringUtils;
 
if (StringUtils.isNotBlank(topic.getString())) {
    generator.writeStringField("string", topic.getString());
}

Регистрация сериализатора производится в классе-конфиге:

@Configuration
public class Config {
...
	@Bean
	public RestOperations restOperations() {
		...
		MappingJackson2HttpMessageConverter jsonHttpMessageConverter = new MappingJackson2HttpMessageConverter();
    	jsonHttpMessageConverter.getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
    	jsonHttpMessageConverter.getObjectMapper().configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    	jsonHttpMessageConverter.getObjectMapper().registerModule(new JavaTimeModule());
 
		SimpleModule module = new SimpleModule();
 
    	module.addSerializer(Topic.class, new TopicSerializer());
    	// other serializers and deserializers get registered
    	jsonHttpMessageConverter.getObjectMapper().registerModule(module);
		...
	}
}
You can leave a response, or trackback from your own site.

Leave a Reply