"Dynamic" render functions in views that don't error out when fields or associations aren't loaded

One of the major issues I have with Phoenix is with views. The render function expects you to pre-define the structure of the JSON response in the form of a map, which in turn is mapped to the fields of a data structure that is fed to the function. If a field or association is missing from that data structure — maybe because it wasn’t selected or preloaded — the entire thing just freaks out and the server returns a 500 error.

An abbreviated example from InvoiceView:

  def render("invoice_with_customer.json", %{invoice: invoice}) do
    %{id: invoice.id,
        po_number: invoice.po_number,
        status: invoice.status,
        invoice_date: invoice.invoice_date,
        total: Decimal.to_string(invoice.total),
        customer: render_one(invoice.customer, CustomerView, "customer.json")
    }
  end

Here, if I don’t select the po_number column or preload the associated customer when fetching the invoice from the database, an error is thrown. I then need to go to the context and modify the Repo function to make sure that data is fetched.

This introduces a need to “synchronize” the view layer with the context layer, and what it has resulted in for me, over the years, is a “rat’s nest” of several hundred render functions inside each context that are tightly coupled, both to their contexts and also to render functions in associated contexts. There is just_invoice.json when rendering just the invoice with no associations, invoice_with_customer.json when rendering an invoice with its customer, invoice_with_customer_and_company.json when rendering an invoice with customer and the company that customer belongs to, and so on. And if at some point I add a field to customer.json inside the CustomerView, I need to make sure to check what other views are referencing it, then trace each of their functions all the way back to their contexts (through the controller layer) and modify those to make sure they fetch that field or association during preload.

Suffice it to say, this makes refactoring and debugging very painful, especially when dealing with rendering nested associations that may be several layers deep.

I know you can simply render a given struct/map “as is” using json/2 instead of render, but that has the opposite problem: not enough control. A lot of the time, field data needs to be transformed in some way by getting passed through helper functions, such as with the total field above.

What I’m looking for, basically, is a dynamic strategy for rendering data, such as "if you define fields A, B and C and association D in the view, and the data fetched from the database only has A and B, then just render those and don’t complain about C and D :grinning:. Or some other solution that doesn’t involve substantial engineering effort simply to push this complexity to another layer (e.g. GraphQL).

https://hexdocs.pm/ecto/Ecto.html#assoc_loaded?/1

This should allow you to react to not loaded associations however you need.