Serializers avançados
Associação aninhada, atributo condicional por contexto e N+1 escondido dentro de um serializer — o que a lição básica deixou de fora.
A lição de serializers cobriu atributos explícitos e um atributo calculado. Faltam três coisas que aparecem o tempo todo num serializer de produção: aninhar uma associação inteira dentro da resposta, mudar o que sai dependendo de quem pediu, e evitar que essa mesma associação aninhada dispare uma query por registro.
Associação aninhada — serializando o relacionamento inteiro
class ItemSerializer
include JSONAPI::Serializer
attributes :quantidade
belongs_to :produto, serializer: ProdutoSerializer
end
class PedidoSerializer
include JSONAPI::Serializer
attributes :status, :total
belongs_to :usuario
has_many :itens, serializer: ItemSerializer
endserializer: ItemSerializer diz explicitamente qual serializer usar pra cada item da lista — sem isso, o gem não saberia formatar itens como objetos estruturados. O resultado é uma árvore completa: pedido, com seus itens, com o produto de cada item, tudo no mesmo JSON.
Serializer condicional por contexto
Às vezes o mesmo recurso precisa aparecer diferente dependendo de quem está pedindo — um campo que só administrador vê, por exemplo. Duplicar o serializer inteiro (ProdutoSerializer e AdminProdutoSerializer) só pra um campo a mais é desperdício; a opção if: resolve isso no mesmo serializer.
class ProdutoSerializer
include JSONAPI::Serializer
attributes :nome, :preco
attribute :custo_interno, if: Proc.new { |_produto, params| params[:usuario_atual]&.admin? }
enddef show
render json: ProdutoSerializer.new(
@produto,
params: { usuario_atual: current_usuario }
).serializable_hash
endO Proc recebe o registro e os params passados na hora de instanciar o serializer — se usuario_atual não for admin, custo_interno simplesmente não entra na resposta. Mesma classe, mesmo model, JSON diferente dependendo de quem chamou.
N+1 escondido dentro de um serializer
O perigo de has_many :itens, serializer: ItemSerializer, que por sua vez tem belongs_to :produto, é que serializar uma lista de pedidos assim dispara uma query de itens por pedido, e uma query de produto por item — sem ninguém perceber, porque o N+1 não está no controller, está dentro da árvore de associações do serializer.
def index
render json: PedidoSerializer.new(Pedido.all).serializable_hash
# pra cada pedido: 1 query de itens + 1 query de produto por item
enddef index
pedidos = Pedido.includes(itens: :produto)
render json: PedidoSerializer.new(pedidos).serializable_hash
endincludes(itens: :produto) carrega pedidos, itens e produtos em poucas consultas otimizadas, em vez de uma nova consulta pra cada registro percorrido. O serializer em si não muda nada — ele não sabe (nem precisa saber) se os dados vieram eager loaded; o ajuste sempre acontece na query, no controller.
Exercício
Um UsuarioSerializer tem attributes :nome. Adicione um atributo condicional email, visível só quando usuario_atual (passado via params) for o próprio usuário sendo serializado ou for admin.