How do I handle unloaded associations when rendering JSON responses?

Rendering Ecto structs has been a major pain point for me. Any help is greatly appreciated.

Given an Orders schema with a belongs_to relation to Customer, and a many_to_many relation to Product, how do I go about rendering these associations using Jason? I can’t simply do something like this:

  defp data(%Order{} = order) do
    order
  end

because Jason complains that this struct hasn’t implemented the Jason.Encoder protocol.

I could simply make the schemas implement the protocol, but I’ve done this before and if you end up changing a structure of the struct in any way you’re back to the start.

Ideally, I would like the metadata of the struct (e.g., __struct__, __meta__, any keys with the value of Ecto.Association.NotLoaded) to get filtered out automatically.

I ended up creating this abomination:

  defp data(%Order{} = order) do
    customer = Map.get(order, :customer)

    customer =
      if not is_struct(customer, Ecto.Association.NotLoaded) do
        %{
          id: customer.id,
          name: customer.name,
          delivery_address: customer.delivery_address
        }
      end

    products = Map.get(order, :products)

    products =
      if not is_struct(products, Ecto.Association.NotLoaded) do
        for product <- order.products do
          %{
            id: product.id,
            name: product.name,
            price: product.price
          }
        end
      end

    %{
      id: order.id,
      payment_method: order.payment_method,
      notes: order.notes,
      status: order.status,
      customer: customer,
      products: products
    }
  end

Is there a way most people handle this issue?

Your order is not a Map, but a Struct with known fields, so I’d refrain from using Map.get, but use the Access behaviour.

Then you can use Ecto.assoc_loaded?/1.

customer = if Ecto.assoc_loaded?(order.customer) do ...