Seeking advice on handling preloaded associations in JSON views

Hi,

I have a JSON view module for a Criterion schema that handles optional preloaded associations. Here’s my current implementation:

defmodule CockpitWeb.CriterionJSON do
  alias Cockpit.Certification.Criterion
  alias CockpitWeb.{ProgramJSON, VariantJSON, ValueJSON, RequirementJSON}

  def data(%Criterion{} = criterion) do
    %{
      id: criterion.id,
      code: criterion.code,
      # ... other base fields ...
      program_id: criterion.program_id,
      variant_id: criterion.variant_id
    }
    |> maybe_add_program(criterion)
    |> maybe_add_variant(criterion)
    |> maybe_add_value(criterion)
    |> maybe_add_requirement(criterion)
  end

  defp maybe_add_program(data, criterion) do
    if Ecto.assoc_loaded?(criterion.program) do
      Map.put(data, :program, ProgramJSON.data(criterion.program))
    else
      data
    end
  end

  ...

  # Similar pattern for variant, value, and requirement
  defp maybe_add_value(data, criterion) do
    if Ecto.assoc_loaded?(criterion.values) do
      data
      |> Map.drop([:values])
      |> Map.put(:value, extract_value(criterion.values))
    else
      data
    end
  end

  defp extract_value(values) do
    case values do
      [value | _] -> ValueJSON.data(value)
      _ -> nil
    end
  end
end

My current approach works but feels verbose for a few reasons:

  1. I have multiple maybe_add_xxx functions that follow the same pattern
  2. Some associations (like values and requirements) come back as lists from left_join even though I know they only contain one item at most
  3. Each association requires both a check for assoc_loaded? and handling the data transformation

Is there a more idiomatic or concise way to handle this pattern in Elixir? I’d especially appreciate suggestions for:

  • Reducing the repetitive maybe_add_xxx functions
  • Handling single-item associations that come back as lists
  • Maintaining flexibility for different preloading scenarios

Sounds like these should be has_one rather than has_many.

More generally I’d say the problem is not necessarily complicated because of the way you handle the view, but because how complicated the input is. Why does this view function need to deal with 4 optional preloads? Why is there no distinction between those different sets of data at an earlier level of your stack?

You are totally right, my fault !

Well, one criterion belong to a program and a variant. It can have a value and a requirement for a particular project/version.

  • When listing the existing criteria, I need to show the program name and variant name in a listing table.
  • But when retrieving the criteria in a project/version context, I need to retrieve the value and the requirement instead.

I’m unsure where I can make this distinction higher. I’m using the CriterionJSON in two distincts controllers: /api/programs/:id/criteria and /api/projects/:id/criteria where the satellite info around a criterion differ. Perhaps this is a mistake.