Ecto type storing a Coelho document in a :map (jsonb) column.
The type carries the schema, so validation happens where Ecto already reports failures — in the changeset:
schema "posts" do
field :body, Coelho.Ecto.Type, document_schema: MyApp.RichText.schema()
endThe option is :document_schema and not :schema because Ecto injects
its own :schema key — the owning Ecto schema module — into every
parameterized type's options.
Coelho.Ecto.rich_text/2 is the shorter way to write the same thing.
What casting accepts
- a document map, as
Jason.decode/1or ProseMirror'stoJSON()produce it — validated and normalised - a JSON string, which is what a form posts back from the editor's hidden input — decoded, then validated
niland"", which cast tonil
A document failing validation makes the changeset invalid rather than raising, and the individual schema violations are attached to the error so a form can show them:
{:error, changeset} = MyApp.Posts.create(%{body: hostile})
changeset.errors
#=> [body: {"is invalid rich text",
#=> [validation: :coelho,
#=> human: "block 1: unknown node type \"script\"",
#=> errors: ["content[0]: unknown node type \"script\""]]}]Loading
Values already in the database are loaded without re-validating them.
A schema that grew stricter after rows were written would otherwise make
old rows unreadable, which is a migration to run deliberately, not a
failure to discover at read time. Renderers are written accordingly:
Coelho.Render never derives markup structure from stored values.
That is a floor, not a guarantee about the row. A document written under
a looser schema, or by a direct SQL write, is not something cast/3 ever
saw. Put it through Coelho.Document.sanitize/2 before rendering it
somewhere a reader will see:
post.body
|> Coelho.sanitize(MyApp.RichText.schema())
|> Coelho.to_html(MyApp.RichText.schema())