Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to configure Jackson deserializer for nested entites with Spring Boot

Consider the following entities:

package br.com.investors.domain.endereco;

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkArgument;
import static javax.persistence.GenerationType.SEQUENCE;

@Entity
public class Regiao implements Serializable, Comparable<Regiao> {

    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;

    @Version
    private Long version;

    @NotBlank
    @Column(length = 100, unique = true)
    private String nome = "";

    Regiao() {}

    public Regiao(String nome) {
        checkArgument(!Strings.isNullOrEmpty(nome), "Nome não pode ser vazio");
        this.nome = nome;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Regiao) {
            Regiao o = (Regiao) obj;
            return Objects.equal(this.nome, o.nome);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(nome);
    }

    @Override
    public int compareTo(Regiao o) {
        return ComparisonChain.start()
                .compare(this.nome, o.nome)
                .result();
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(getClass()).add("nome", nome).toString();
    }

    public Long getId() {
        return id;
    }

    public Long getVersion() {
        return version;
    }

    public String getNome() {
        return nome;
    }
}

and

package br.com.investors.domain.endereco;

import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ComparisonChain;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.io.Serializable;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static javax.persistence.GenerationType.SEQUENCE;

@Entity
public class Cidade implements Serializable, Comparable<Cidade> {

    @Id
    @GeneratedValue(strategy = SEQUENCE)
    private Long id;

    @Version
    private Long version;

    @NotBlank
    @Column(length = 100, unique = true)
    private String nome = "";

    @NotNull
    @ManyToOne
    private Regiao regiao;

    @NotNull
    @ManyToOne
    private Estado estado;

    Cidade() {}

    public Cidade(String nome, Regiao regiao, Estado estado) {
        checkArgument(!Strings.isNullOrEmpty(nome), "Nome não pode ser vazio");
        checkNotNull(regiao, "Região não pode ser nulo");
        checkNotNull(estado, "Estado não pode ser nulo");

        this.nome = nome;
        this.regiao = regiao;
        this.estado = estado;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof Cidade) {
            Cidade o = (Cidade) obj;
            return Objects.equal(this.nome, o.nome) &&
                    Objects.equal(this.estado, o.estado) &&
                    Objects.equal(this.regiao, o.regiao);
        }
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(nome, regiao, estado);
    }

    @Override
    public int compareTo(Cidade o) {
        return ComparisonChain.start()
                .compare(this.estado, o.estado)
                .compare(this.regiao, o.regiao)
                .compare(this.nome, o.nome)
                .result();
    }

    @Override
    public String toString() {
        return Objects.toStringHelper(getClass()).add("nome", nome).add("regiao", regiao).add("estado", estado).toString();
    }

    public Long getId() {
        return id;
    }

    public Long getVersion() {
        return version;
    }

    public String getNome() {
        return nome;
    }

    public Regiao getRegiao() {
        return regiao;
    }

    public Estado getEstado() {
        return estado;
    }
}

I'm trying to POST a JSON to a RestController

@RequestMapping(value = "/cidades", method = POST, consumes = APPLICATION_JSON_VALUE)
void inserir(@RequestBody Cidade cidade) {
    repository.save(cidade);
}

I'm using default configurations of Spring Boot to serialize and deserialize objects.

If I post a JSON like this, it works fine:

{
    "nome": "Cidade",
    "regiao": "/10"
}

But I need to post a JSON like this:

{
    "nome": "Cidade",
    "regiao": {
        "id": 10,
        "version": 0,
        "nome": "regiao"
    }
}

If I do so, I get the error

{
    "timestamp": "2015-04-02",
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    "message": "Could not read JSON: Template must not be null or empty! (through reference chain: br.com.investors.domain.endereco.Cidade[\"regiao\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: br.com.investors.domain.endereco.Cidade[\"regiao\"])",
    "path": "/cidades/"
}

Doing some debug, I found that Jackson tries to create a URI from the "regiao" property of the posted object, waiting for a string template like "/{id}". I'm googling it but can't find a properly answer for this.

I saw some related issues on StackOverflow but none worked for me.

Can you guys say what is the matter of this?

I think that is just a configuration but don't know how or where.

I'm also trying to avoid custom serializers and deserializers.

EDIT:

If I POST a JSON with only the ids of the nested entities, like this:

{
  "nome": "Cidade",
  "estado": "10",
  "regiao": "10"
}

I get this message:

{
    "timestamp": "2015-04-07",
    "status": 400,
    "error": "Bad Request",
    "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    "message": "Could not read JSON: Failed to convert from type java.net.URI to type br.com.investors.domain.endereco.Estado for value '10'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI 10. Is it local or remote? Only local URIs are resolvable. (through reference chain: br.com.investors.domain.endereco.Cidade[\"estado\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to convert from type java.net.URI to type br.com.investors.domain.endereco.Estado for value '10'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI 10. Is it local or remote? Only local URIs are resolvable. (through reference chain: br.com.investors.domain.endereco.Cidade[\"estado\"])",
    "path": "/cidades"
}

As I see that the correct way of send the nested entity is like "regiao": "/10", I'm hardcoding this in my Javascript to workaround:

function(item) {
    item.regiao = "/" + item.regiao.id; //OMG
    item.estado = "/" + item.estado.id; //OMG!!

    if (item.id) {
        return $http.put('/cidades/' + item.id, item);
    } else {
        return $http.post('/cidades', item);
    }
}

It works but it sucks. How can I fix this in Javascript or configuring Jackson?

Reading some docs, there is something to do with UriToEntityConverter, but still don't know the correct way of configure this.

Thanks.

like image 325
Jonathan Oliveira Avatar asked Apr 02 '15 16:04

Jonathan Oliveira


1 Answers

I solved it with @RestResource(exported = false) annotation on the EstadoRepository and RegiaoRepository classes.

It "hides" de repo from spring when it's auto config the endpoints and stuff...

like image 59
Jonathan Oliveira Avatar answered Oct 04 '22 18:10

Jonathan Oliveira