Scope
One Mix library and one Hex package (formata, namespace Formata) with no
runtime dependencies. It composes backends that are maintained forks of
existing packages (see "Forked backends"); it adds no processes, caches,
umbrella, schema AST, schema inference, or schema conversion.
| Module | Responsibility |
|---|---|
Formata | Public entry points: decode/3, encode/3, convert/4, validate/3, cast/3 |
Formata.Codec | Codec behaviour, format lookup, shared value-domain checks |
Formata.Codec.JSON / .TOML / .YAML | Format adapters |
Formata.Codec.TOML.Encoder | Focused TOML 1.0.0 encoder |
Formata.Schema | compile/3, check/3, compiled-schema struct |
Formata.Schema.Native | Formata.Backplane.Schema adapter, pure-validation preflight, casting |
Formata.Schema.JSONSchema | Formata.Backplane.JsonSchema adapter |
Formata.Transform | Explicit data transformations |
Formata.Error | Structured errors |
Contracts
- Argument order is data first:
decode(input, format, opts),encode(data, format, opts),convert(input, from, to, opts),validate(data, schema, opts),cast(data, schema, opts),Schema.compile(kind, definition, opts). - Producing operations return
{:ok, result};validate/3andSchema.check/3return:ok. - Expected failures return
{:error, [%Formata.Error{}, ...]}(non-empty). Error kinds::parse,:encode,:schema,:validation,:transform,:unsupported. Programming errors (wrong argument types, unknown compile options) raise. locationis set only from backend data (Formata.Jasonbyte offsets,Formata.Tomlline/column in messages, yamerl line/column).validate/3requires a compiled schema; compiling is never repeated per call.
Forked backends
The backends were published packages; they are now forked into this
repository, renamed into the Formata namespace (so a consumer can still use
the originals side by side), and maintained here. Upstream test suites are
ported under test/formata/vendor/. Provenance, licenses, and the common
fork-time modifications are in NOTICE.md.
| Upstream | Version | Fork | Use |
|---|---|---|---|
jason | 1.4.5 | Formata.Jason | JSON decode (objects: :ordered_objects for duplicate detection) and encode |
gsmlg_toml | 1.0.0 | Formata.Toml | TOML decode |
yaml_elixir | 2.12.2 | Formata.YamlElixir | YAML decode (read_all_from_string/2) |
yamerl | 0.10.0 | formata_yamerl* (Erlang) | Detailed node tree for duplicate keys, merge keys, null roots |
ymlr | 5.1.6 | Formata.Ymlr | YAML encode |
backplane_schema | 1.10.76 | Formata.Backplane.Schema | Native schemas (public vocabulary) |
backplane_json_schema | 1.10.76 | Formata.Backplane.JsonSchema | JSON Schema compile/validate |
ex_json_pointer | 0.9.0 | Formata.ExJSONPointer | JSON Pointer for the JSON Schema backend |
Fork rules: no compile-time optional integrations (their presence would depend
on the consumer's dependency tree), no application environment, no processes.
The minimum Elixir is 1.18 (built-in JSON, used by the JSON Schema fork);
CI tests Elixir 1.18 and 1.20.
Audit findings and decisions
Formata.Backplane.Schema.validate/3 transforms
It applies defaults, coercions and transforms, and in strict mode drops undeclared fields and re-keys output by schema keys; permissive mode leaves the input keys and writes updated values under the data's own key (fork fix for gsmlg-opt/backplane#171, which duplicated them under atom keys). There is no pure mode and no reject-unknown mode.
Decision: pure validation uses a plan, a rewritten definition that only adds checks:
- A preflight walks the definition and every reachable
{:ref, {Mod, name}}target and rejectsdefault,transform,encode,coerce, and{:dependent, callback}with:unsupportederrors and schema paths. {:custom, v}validators are wrapped so a result other than:ok/error is a:non_pure_customerror. Without this, the native engine accepts{:enum, choices, type: {:custom, fn -> {:ok, changed} end}}by checking the changed value.- References become lazily expanded
{:dependent, fun}nodes so the wrapping also applies inside referenced (including recursive) definitions. unknown: :rejectwraps each map schema as{:schema, map, {:additional_keys, {:custom, reject}}}, keeping the check in the native engine; Formata expands the resulting error to one:unknown_fielderror per undeclared key.- The plan runs in permissive mode; its output is discarded.
cast/3 runs the original definition (strict mode for :strip, the reject
plan in strict mode for :reject, permissive mode for :allow, keeping the
input keys; a field added by a default uses the schema key). Coercion to :atom!
is refused because it calls String.to_atom/1 on input.
JSON Schema
Documents go straight to Formata.Backplane.JsonSchema.compile/2; the native
from_json_schema importer (which turns oneOf into a first-match union) is
not used, nor is native export (which omits unsupported constraints). No
schema export is offered: the fork's native to_json_schema/2
and from_json_schema/2 functions are @doc false (kept only for the ported upstream
tests) and are not part of the public API.
- Dialects: Draft 7 and 2020-12 via
$schemaordialect:; conflicts are backend:draft_conflicterrors (:dialect_conflict). An unrecognized$schemawithoutdialect:or:loaderis rejected, because its vocabularies cannot be known. format: annotation by default, including without$schema(fork fix for gsmlg-opt/backplane#170: the default vocabularies are the standard 2020-12 set, without format-assertion). Values that need the IDNA2008 tables (A-labels, non-ASCII labels inhostname/email/idn-*) stop validation with:indeterminate_format, reported as:unsupported/:unsupported_format; it cannot be inverted bynot/anyOf/oneOf/if.multipleOfuses exact decimal arithmetic on integers (noDecimal, no float quotient); it never raises on data.- References: local only unless a
:loaderis passed. Unresolvable refs are backend validation errors that Formata reports as:schema/:unresolved_ref. validate/3accepts no options, so nothing can overrideadditionalProperties/unevaluatedProperties.- Data must be in the JSON data model; other terms are
:non_json_value. - Error messages:
format_error/1is called without the path becauseFormata.Errorrenders it (the backend's path rendering, #172, is fixed in the fork). - Opaque base URIs (
urn:...) resolve relative references per RFC 3986 on every supported Elixir version (Elixir < 1.20'sURI.merge/2requires a host).
Codecs
- JSON:
Formata.Jasonsilently keeps the last duplicate key; Formata decodes ordered objects and rejects duplicates. - TOML:
gsmlg_tomlhad no encoder, so Formata ships its own. The forked decoder had four bugs found by toml-test (gsmlg-dev/gsmlg_umbrella#13 offset sign, #14 empty array-of-tables elements, #15true/falsekeys, #16 local date-time in arrays) plus many more exposed by the invalid suite (lexer escapes, table redefinition rules, inline tables, crashes on invalid UTF-8); all are fixed. The decoder passes the full toml-test 1.0.0 suite (208 valid, 509 invalid); encoder output is also checked with Python'stomllib. - YAML:
Formata.YamlElixir.read_from_string/2silently returns the last document, keeps the first duplicate key, renames<<keys, and maps a null root to%{}. Formata usesread_all_from_string/2and inspects yamerl's detailed node tree.Formata.Ymlrleaves some strings unquoted that read back as numbers (for example".5"), so YAML output is decoded and compared before it is returned. - Special floats use the atoms produced by
Formata.Toml(:infinity,:negative_infinity,:nan); YAML's:"+inf"/:"-inf"are normalized to them and converted back forFormata.Ymlr.