JSON validation with defaults

I’ve seen a few examples on using Ecto.Schema for validations but I can’t seem to find a way to make defaults play nicely. For some background, I’m building a API that aggregate and cleans data before POSTing it to an external HTTP service. Since the external service has tons of optional params and others that I can provide defaults to I wanted to use a_emphasized text_ schema.

defmodule Lead do
  use Ecto.Schema

  import Ecto.Changeset

  @required_fields ~w(device type_id website)a

  @primary_key false
  embedded_schema do
    field :device, :string
    field :type_id, :integer, default: 0
    field :session_id, :string
    field :website, :string, default: "facebook"
    field :message, :string
  end

  def changeset(%__MODULE__{} = lead, params \\ %{}) do
    lead
    |> cast(params, __schema__(:fields))
    |> cast_session_id
    |> validate_required(@required_fields)
    |> validate_inclusion(:device, ["web", "mobile"])
    |> validate_inclusion(:type_id, 0..6)
  end

  defp cast_session_id(changeset) do
    case get_field(changeset, :session_id) do
      nil ->
        unix_time =
          DateTime.utc_now
          |> DateTime.to_unix(:milliseconds)

        put_change(changeset, :session_id, "no_session_id_#{unix_time}")

      _ -> changeset
    end
  end
end

This works fine for the most part but when trying to get the final map I run into some weirdness.

iex(1)> cs = %Lead{} |> Lead.changeset(%{device: "web"})
#Ecto.Changeset<action: nil,
 changes: %{device: "web", session_id: "no_session_id_1503097557106"},
 errors: [], data: #Lead<>, valid?: true>

iex(2)> cs.changes
%{device: "web", session_id: "no_session_id_1503097557106"}

iex(3)> Ecto.Changeset.apply_changes(cs)
%Lead{device: "web", message: nil, session_id: "no_session_id_1503097557106",
 type_id: 0, website: "facebook"}

If I try and use the changes, then field defaults don’t work.
If I try to apply_changes I get fields with nil.

I see two options, I could either move the default into a cast like cast_session_id or I could just filter out keys with nil values. The real schema has 15 optional fields with 5 or so fields that I could provide defaults.

Thoughs / Suggestions?

This is what you should do, and message is nil because it has no default value and it was not set. If it should be set by the user then you need to add it to the validate_requited call. :slight_smile:

1 Like

Thanks for the reply.

I thought about this a little more over the weekend and it does make sense for apply_changes to return fields with nil, in the context of struct access. I guess I was stuck thinking about it as post params, where you wouldn’t necessarily want to pass in nil for optional fields.

For this request, nil can only mean missing or unset so I should be good if I filter out nil.

1 Like