docs: backfill TOC + decision trees, fix review findings

- Add ## Contents and ## Decision Tree to all 10 existing pattern files
- Fix embed_as/1 semantics inversion in types.md (:self → :dump)
- Fix fabricated __meta__.changes reference in changesets.md
- Fix default primary key type (:integer → :id) in schemas.md
- Combine @impl subsections into single "Minimal Callback Annotation"
This commit is contained in:
2026-05-01 22:13:35 -07:00
parent b33accf37c
commit 10218813d3
13 changed files with 356 additions and 87 deletions
+9 -9
View File
@@ -135,21 +135,21 @@ end
When a custom type is used inside an `embeds_one` or `embeds_many` field, Ecto calls `embed_as/1` to decide whether to pass the value through `dump/1` or treat it as its own serialized form. The callback receives the embed format (`:json` by default) and returns either `:self` or `:dump`.
- `:self` — the value is used as-is when exporting embedded data (dump is still called for DB storage)
- `:dump``dump/1` is always called, even in embedded contexts
- `:self` — the in-memory value is used as-is without calling `dump/1`; appropriate when the runtime representation is already JSON-compatible (scalars, plain maps)
- `:dump``dump/1` is called to serialize the value before encoding; needed when the runtime representation (e.g., a struct) is not directly JSON-serializable
`use Ecto.Type` provides a default implementation that returns `:self`. Override it when your type must always run `dump/1` to produce its storable form, even when nested inside an embedded schema.
`use Ecto.Type` provides a default implementation that returns `:self`. Override it to return `:dump` when your type holds an Elixir struct or other value that cannot be directly encoded to JSON.
```elixir
defmodule EctoURI do
use Ecto.Type
# Override to ensure dump/1 is called in embedded contexts
def embed_as(_format), do: :self
# Override: %URI{} is not JSON-serializable, so run dump/1 in embedded contexts
def embed_as(_format), do: :dump
end
```
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), returning `:self` without a proper `dump/1` would persist the raw struct map rather than your intended shape. Overriding `embed_as/1` makes the contract explicit.
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), the default `:self` passes the raw struct to the JSON encoder — which either raises or produces garbage like `%{__struct__: "Elixir.URI", host: ..., ...}`. Returning `:dump` ensures `dump/1` converts it to a clean map first.
**Anti-pattern:** Assuming the default `:self` is correct for a type whose in-memory and storable representations differ, then debugging mysterious embedded schema corruption:
```elixir
@@ -199,8 +199,8 @@ defmodule EctoURI do
def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
def dump(_), do: :error
# Explicit: always pass through dump/1 when exporting embedded values
def embed_as(_format), do: :self
# Explicit: ensure dump/1 runs in embedded contexts to produce a clean map
def embed_as(_format), do: :dump
end
```
@@ -209,7 +209,7 @@ end
**Don't use this when:**
- Your type is never used in embedded schemas (the callback has no effect)
- The in-memory and storable representations are the same (plain maps, scalars)
- You intentionally want to skip `dump/1` in embedded contexts (then return `:dump` and document why)
- You intentionally want to skip `dump/1` in embedded contexts (the default `:self` already does this)
**Over-application example:**
```elixir