[RESOLVIDO] Como resumir o retorno de um JSON em uma aplicação Spring Boot

Prezados, bom dia!

Estou tentando encontrar uma forma de simplificar o retorno JSON de uma consulta através de uma requisição GET para meu endpoint: /pizzas

Ocorre que essa consulta me retorna a seguinte estrutura padrão:

{
    "id": 1,
    "tamanho": {
        "id": 3,
        "descricao": "GRANDE",
        "valor": 40,
        "tempo": 25
    },
    "sabor": {
        "id": 2,
        "descricao": "Marguerita",
        "tempo": 0
    },
    "adicionais": [
        {
            "id": 2,
            "descricao": "Sem Cebola",
            "valor": 0,
            "tempo": 0
        }
    ],
    "valor": 40,
    "tempo": 25
}

Como eu poderia fazer para obter este formato de retorno abaixo, como se fosse um resumo:

{
    "id": 1,
    "tamanho": {
        "descricao": "GRANDE"
    },
    "sabor": {
        "descricao": "Marguerita"
    },
    "adicionais": [
        {
            "descricao": "Sem Cebola"
        },
        {
            "descricao": "Extra Bacon"
        },
        {
            "descricao": "Outro adicional qualquer"
        }
    ],
    "valor": 40,
    "tempo": 25
}

Os códigos e classes estão disponíveis no meu github: https://github.com/jonathanmdr/pizzaria-api

Sugestões e exemplos são muito bem vindos!

Você pode usar o atributo @JsonIgnore nos atributos que vc nao quer retornar ou pode montar uma classe apenas com os atributos que quer retornar (recomendo).

1 curtida

Estava pesquisando uma coisa diferente do tema, mas, achei isso: https://dzone.com/articles/spring-web-service-response-filtering

1 curtida

Agradeço pelas respostas meu caros!

Consegui realizar 90% do que eu queria utilizando os recursos do Spring Data com Criteria Query a partir de um Metamodel, o código está disponível no link do repositório no tópico da pergunta.

Porém tive um problema, dentro da minha classe Pizza eu possuo um atributo do tipo Set<Adicional>, que é uma lista de adicionais contidos em uma pizza, ao tentar incluir esse atributo na projeção, o mesmo não é carregado com seus valores igual os demais campos da projeção, ficando então com valor nulo e lançando o famoso NullPointerException, porém não consegui identificar onde está a falha, caso possam me auxiliar ficarei muito grato!

Consegui localizar o erro principal, segue abaixo:
Illegal argument on static metamodel field injection : com.uds.pizzaria.model.Pizza_#adicionais; expected type : org.hibernate.jpa.internal.metamodel.PluralAttributeImpl$SetAttributeImpl; encountered type : javax.persistence.metamodel.SingularAttribute

Vocês tem alguma ideia de como eu poderia resolver isso, para que seja possível recuperar essa lista de adicionais na projeção?

Como vc fez a consulta usando esse Metamodel?

Esqueci de atualizar o tópico, desculpe, consegui resolver esse problema dos erros, está gerando a projeção corretamente, sem erros, porém não da forma que eu esperava.

Exemplo: Tenho uma pizza com 3 adicionais, o valor que eu queria gerar na projeção é o seguinte:

{
	"id": 3,
	"tamanho": "GRANDE",
	"sabor": "Portuguesa",
	"adicionais": [ 
		{
			"descricao": "Extra Bacon"
		},
		{
			"descricao": "Sem Cebola"
		},
		{
			"descricao": "Borda Recheada"
		},
	],
	"valor": 48,
	"tempo": 35
}

Porém ele está dividindo um resultado para cada adicional, ficando da seguinte forma:

{
	"id": 3,
	"tamanho": "GRANDE",
	"sabor": "Portuguesa",
	"adicionais": {
		"id": 1,
		"descricao": "Extra Bacon",
		"valor": 3,
		"tempo": 0
	},
	"valor": 48,
	"tempo": 35
},
{
	"id": 3,
	"tamanho": "GRANDE",
	"sabor": "Portuguesa",
	"adicionais": {
		"id": 2,
		"descricao": "Sem Cebola",
		"valor": 0,
		"tempo": 0
	},
	"valor": 48,
	"tempo": 35
},
{
	"id": 3,
	"tamanho": "GRANDE",
	"sabor": "Portuguesa",
	"adicionais": {
		"id": 3,
		"descricao": "Borda Recheada",
		"valor": 5,
		"tempo": 5
	},
	"valor": 48,
	"tempo": 35
}

Minha classe Pizza está da seguinte forma:

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = {"id"})
@ToString
@Entity
@Table(name = "pizza")
public class Pizza {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @ManyToOne
    @JoinColumn(name = "idtamanho")
    private Tamanho tamanho;

    @NotNull
    @ManyToOne
    @JoinColumn(name = "idsabor")
    private Sabor sabor;

    @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.MERGE)
    @JoinTable(name = "pizza_adicionais", joinColumns = @JoinColumn(name = "idpizza"), inverseJoinColumns = @JoinColumn(name = "idadicional"))
    private Set<Adicional> adicionais;

    private Long valor;
    private Long tempo;

}

Meu Metamodel de pizza está da seguinte forma:

@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor")
@StaticMetamodel(Pizza.class)
public class Pizza_ {

    public static volatile SingularAttribute<Pizza, Long> id;
    public static volatile SingularAttribute<Pizza, Tamanho> tamanho;
    public static volatile SingularAttribute<Pizza, Sabor> sabor;
    public static volatile SetAttribute<Pizza, Adicional> adicionais;
    public static volatile SingularAttribute<Pizza, Long> valor;
    public static volatile SingularAttribute<Pizza, Long> tempo;

}

Minha classe de projeção está da seguinte forma:

@Getter
@Setter
@AllArgsConstructor
public class ResumoPizza {

    private Long id;
    private String tamanho;
    private String sabor;
    private Adicional adicionais;
    private Long valor;
    private Long tempo;

}

Minha classe RepositoryImpl:

public class PizzaRepositoryImpl implements PizzaRepositoryQuery {

    @PersistenceContext
    private EntityManager manager;

    @Override
    public Page<ResumoPizza> resumir(Pageable pageable) {
        CriteriaBuilder builder = manager.getCriteriaBuilder();
        CriteriaQuery<ResumoPizza> criteria = builder.createQuery(ResumoPizza.class);

        Root<Pizza> root = criteria.from(Pizza.class);

        criteria.select(builder.construct(ResumoPizza.class,
                root.get(Pizza_.id),
                root.get(Pizza_.tamanho).get(Tamanho_.descricao),
                root.get(Pizza_.sabor).get(Sabor_.descricao),
                root.join(Pizza_.adicionais),
                root.get(Pizza_.valor),
                root.get(Pizza_.tempo)));

        TypedQuery<ResumoPizza> query = manager.createQuery(criteria);
        adicionarRestricoesDePaginacao(query, pageable);

        return new PageImpl<>(query.getResultList(), pageable, total());
    }

    private void adicionarRestricoesDePaginacao(TypedQuery<?> query, Pageable pageable) {
        int paginaAtual = pageable.getPageNumber();
        int totalRegistrosPorPagina = pageable.getPageSize();
        int primeiroRegistroDaPagina = paginaAtual * totalRegistrosPorPagina;

        query.setFirstResult(primeiroRegistroDaPagina);
        query.setMaxResults(totalRegistrosPorPagina);
    }

    private Long total() {
        CriteriaBuilder builder = manager.getCriteriaBuilder();
        CriteriaQuery<Long> criteria = builder.createQuery(Long.class);
        Root<Pizza> root = criteria.from(Pizza.class);

        criteria.select(builder.count(root));

        return manager.createQuery(criteria).getSingleResult();
    }

}

O que será que fiz de errado para gerar o resultado dessa forma?

Pela minha experiencia, é muito mais simples cada serviço ter sua própria estrutura de dados, somente com o necessário pra atender a funcionalidade. Inclusive diminuindo as chances de um serviço impactar o outro.

1 curtida

Mas é uma estrutura única, esse código de projeção responde a um único endpoint quando este recebe o parâmetro “resumo” em sua requisição, e não impacta em mais nada dentro da aplicação.

A não ser que eu tenha interpretado de forma errada o que você disse.

Quis dizer pra sua estrutura de dados ser exatamente como o json enxuto que deseja retornar.

1 curtida

Entendi, agora ficou mais claro pra mim o que você tinha dito!

Foi exatamente isso que eu tentei fazer, porém não obtive sucesso, o resultado gerado é diferente do que eu esperava, e é aí que entra a seguinte questão, onde eu estou errando!?

Agora eu que não entendi. Se tenho uma classe responsável pelo retorno desse serviço, contendo somente os atributos necessários, não tem como vir diferente do esperado.

O que o @javaflex sugeriu é que você crie uma estrutura de classes semelhante a que já possui, porém, contendo, apenas, os dados que deseja transitar.

2 curtidas

O que o @javaflex sugeriu é que você crie uma estrutura de classes semelhante a que já possui, porém, contendo, apenas, os dados que deseja transitar.

Exatamente @darlan_machado.

@Jonathan_Medeiros, seria como se fosse DTO, embora eu não costume me prender a essas siglas, importante dar um nome significativo pra atender a funcionalidade pro negócio, um cardápio, etc.

1 curtida

Esse é o meu problema, não consigo identificar o porquê está vindo diferente, o que foi que fiz de errado no código para estar retornando errado, qual ponto da minha estrutura está fazendo com que o resultado seja quebrado.

Compreendi perfeitamente a ideia do DTO, eu tentei aplicar esse conceito, esse papel está sendo feito pela classe ResumoPizza.
Se observar um pouco mais acima eu coloquei o JSON que eu queria retornar, e o JSON que está sendo retornado.

Boa, então você já tinha evoluído pra isso, eu não tinha visto. Não entendo de Criteria pra saber o que deu de errado, mas faria diretamente via SQL (pode até ser com Hibernate) trazendo somente os dados que preciso para preencher ResumoPizza, por fim gerar um json a partir dessa estrutura.

Hoje com um pouco de tempo consegui sentar e rever este caso que havia publicado aqui no GUJ uns dias atrás e consegui chegar em uma solução, acredito que possam existir melhores soluções para isso mas de momento foi o que me atendeu, caso alguém tenha alguma sugestão de melhoria no código, fico grato pela contribuição!

A quem interessar a solução consta no projeto em meu repositório github: Pizzaria-API