{"id":50863621,"url":"https://github.com/dannote/json_codec","last_synced_at":"2026-06-14T23:05:36.330Z","repository":{"id":363462441,"uuid":"1262061991","full_name":"dannote/json_codec","owner":"dannote","description":"Compile-time generated codecs for JSON-shaped Elixir structs","archived":false,"fork":false,"pushed_at":"2026-06-09T01:49:31.000Z","size":77,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-06-09T03:22:55.748Z","etag":null,"topics":["codec","elixir","hex-package","json","json-schema"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/json_codec","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dannote.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-07T14:21:02.000Z","updated_at":"2026-06-09T01:49:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dannote/json_codec","commit_stats":null,"previous_names":["dannote/json_codec"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/dannote/json_codec","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fjson_codec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fjson_codec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fjson_codec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fjson_codec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dannote","download_url":"https://codeload.github.com/dannote/json_codec/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dannote%2Fjson_codec/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34165764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["codec","elixir","hex-package","json","json-schema"],"created_at":"2026-06-14T23:05:35.741Z","updated_at":"2026-06-14T23:05:36.323Z","avatar_url":"https://github.com/dannote.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JSONCodec\n\nCompile-time generated codecs for JSON-shaped Elixir structs.\n\n`JSONCodec` is **not** another JSON parser. It uses [`Jason`](https://hex.pm/packages/jason) for parsing and focuses on the annoying part that tends to be rewritten in every Elixir project: converting decoded string-keyed JSON maps into nested structs with aliases, defaults, computed fields, explicit atom policy, and schema export.\n\n`JSONCodec` uses normal Elixir declarations as the source of truth:\n\n- `defstruct` for fields and defaults\n- `@type t` for field types\n- `codec/2` only for JSON-specific field metadata\n\n```elixir\ndefmodule FunctionID do\n  use JSONCodec\n\n  defstruct [:module, :function, :arity, :id]\n\n  @type t :: %__MODULE__{\n          module: String.t(),\n          function: String.t(),\n          arity: non_neg_integer(),\n          id: String.t() | nil\n        }\n\n  computed :id, fn function -\u003e\n    \"#{function.module}.#{function.function}/#{function.arity}\"\n  end\nend\n\ndefmodule DataRef do\n  use JSONCodec\n\n  defstruct [:type, :function, :name, :index]\n\n  @type t :: %__MODULE__{\n          type: :argument | :return | :variable,\n          function: FunctionID.t(),\n          name: atom() | nil,\n          index: non_neg_integer() | nil\n        }\n\n  codec :name, atom: :unsafe\nend\n```\n\nGenerated API:\n\n```elixir\nFunctionID.decode!(json)\nFunctionID.decode(json)\nFunctionID.from_map!(map)\nFunctionID.from_map(map)\nFunctionID.to_map(struct)\nFunctionID.schema()\n```\n\nTop-level helpers are also available:\n\n```elixir\nJSONCodec.decode!(json, FunctionID)\nJSONCodec.from_map!(map, FunctionID)\nJSONCodec.schema(FunctionID)\n```\n\n## Why another JSON library?\n\nBecause this is not trying to compete with JSON parsers. It sits after parsing.\n\nMost Elixir JSON code starts with `Jason.decode!/1`, then hand-rolls `from_map!/1` functions forever:\n\n```elixir\ndef from_map!(%{\"from\" =\u003e from, \"to\" =\u003e to} = map) do\n  %DataFlow{\n    from: DataRef.from_map!(from),\n    to: DataRef.from_map!(to),\n    through: Enum.map(Map.get(map, \"through\", []), \u0026DataRef.from_map!/1),\n    variable_names: Enum.map(Map.get(map, \"variable_names\", []), \u0026String.to_atom/1)\n  }\nend\n```\n\n`JSONCodec` generates that boring code from normal struct/typespec declarations.\n\n| Library | Main job | Struct decode | Nested structs | Field aliases | Computed fields | Atom policy | Hot-path goal |\n|---|---|---:|---:|---:|---:|---:|---:|\n| `Jason` | JSON parser/encoder | No | No | No | No | key option only | parsing speed |\n| `Poison` `as:` | parser + old struct decode | Yes | Limited | No | No | key option | legacy parser path |\n| `Spectral` | typespec-driven serialization/schema | Yes | Yes | Yes | via codecs | safe existing atoms | validation/type coverage |\n| `Exdantic`/`Elixact`/`Zoi`/`Drops` | validation frameworks | Sometimes | Yes | Sometimes | Yes | framework-specific | validation UX |\n| `Tarams` | Phoenix params casting | Map output | Nested maps | Yes | transforms | casting-specific | request params |\n| `SimpleSchema` | JSON validation + struct | Yes | Yes | Yes | custom callbacks | limited | validation pipeline |\n| **JSONCodec** | generated JSON-shaped struct codecs | **Yes** | **Yes** | **Yes** | **Yes** | **explicit per field** | **near-handwritten decode** |\n\nUse `Jason` for parsing. Use `Tarams`/`Ecto` for Phoenix params. Use a validation framework when rich validation is the main goal. Use `JSONCodec` when you own the struct shape and want fast, boring, explicit map-to-struct codecs.\n\n## Codec metadata\n\nMost fields need no JSONCodec-specific declaration. Defaults come from `defstruct`; types come from `@type t`.\n\n```elixir\ndefmodule PackageManifest do\n  use JSONCodec, case: :camel, fast_path: :json\n\n  defstruct [:name, :version, dev_dependencies: %{}]\n\n  @type t :: %__MODULE__{\n          name: String.t(),\n          version: String.t() | nil,\n          dev_dependencies: %{String.t() =\u003e String.t()}\n        }\nend\n```\n\n`:camel` maps `:dev_dependencies` to `\"devDependencies\"` automatically.\n\nUse `dump/1` when converting codec-owned structs back to JSON-shaped Elixir data with the configured JSON field names:\n\n```elixir\nmanifest = %PackageManifest{name: \"demo\", dev_dependencies: %{\"jason\" =\u003e \"~\u003e 1.4\"}}\n\nJSONCodec.dump(manifest)\n#=\u003e %{\"name\" =\u003e \"demo\", \"version\" =\u003e nil, \"devDependencies\" =\u003e %{\"jason\" =\u003e \"~\u003e 1.4\"}}\n```\n\n`to_map/1` remains a compatibility helper that stringifies atom keys recursively.\n\n`fast_path: :json` generates an optimized first `from_map!/1` clause for normal `Jason`-decoded JSON maps with string keys. If that fast string-key clause does not match, `JSONCodec` falls back to the full generic decoder, including atom-key lookup and detailed missing-field handling.\n\nUse `codec/2` for exceptions and special behavior:\n\n```elixir\ncodec :not_found, as: \"not_found\"\ncodec :variable_names, atom: :unsafe\ncodec :rotate, transform: :normalize_rotate\n```\n\nLocal callback atoms are expanded to functions in the same module:\n\n```elixir\ncodec :rotate, transform: :normalize_rotate\n# calls normalize_rotate(value)\n\ncodec :icons, values: :icon_value\n# calls icon_value(key, value, source_map)\n```\n\nRemote captures are also supported:\n\n```elixir\ncodec :rotate, transform: \u0026MyTransforms.normalize_rotate/1\ncodec :icons, values: \u0026MyTransforms.icon_value/3\n```\n\n### Advanced map value callbacks\n\nFor map fields, `values:` transforms each raw map value before `JSONCodec` decodes it as the declared value type:\n\n```elixir\ncodec :icons, values: :icon_value\n# icon_value(key, raw_value, source_map) -\u003e raw_value_for_normal_decode\n```\n\nIf that callback needs shared context, use `values_source:` to compute the third argument once per map field:\n\n```elixir\ncodec :icons, values: :icon_value, values_source: :icon_defaults\n# icon_defaults(source_map) -\u003e defaults\n# icon_value(key, raw_value, defaults) -\u003e raw_value_for_normal_decode\n```\n\nFor map-heavy data where a custom decoder is clearer or faster, `decode_values:` returns the final decoded map value directly:\n\n```elixir\ncodec :icons, decode_values: :decode_icon, values_source: :icon_defaults\n# icon_defaults(source_map) -\u003e defaults\n# decode_icon(key, raw_value, defaults) -\u003e final decoded value\n```\n\nRemote captures work for these callbacks too:\n\n```elixir\ncodec :icons, values: \u0026MyTransforms.icon_value/3,\n              values_source: \u0026MyTransforms.icon_defaults/1\n\ncodec :icons, decode_values: \u0026MyTransforms.decode_icon/3,\n              values_source: \u0026MyTransforms.icon_defaults/1\n```\n\nAtom policy is explicit:\n\n```elixir\ncodec :status, atom: :existing\ncodec :variable_name, atom: :unsafe\n```\n\n`:unsafe` uses `String.to_atom/1`; only use it for bounded/trusted internal data.\n\n## Supported type shapes\n\nRead from `@type t`:\n\n- `String.t()`\n- `integer()`\n- `non_neg_integer()`\n- `pos_integer()`\n- `float()`\n- `number()`\n- `boolean()`\n- `atom()`\n- `any()` / `term()`\n- `type | nil`\n- atom unions like `:active | :inactive`\n- `[type]`\n- `%{String.t() =\u003e value_type}`\n- another `JSONCodec` module via `Other.t()`\n\n## Schema export\n\nEach codec module exports a JSON Schema-compatible map:\n\n```elixir\nFunctionID.schema()\nJSONCodec.schema(FunctionID)\n```\n\n`json_schema/0` and `JSONCodec.json_schema/1` are also available as explicit aliases.\n\nThis is intentionally compatible with the direction of `JSONSpec`: codecs are the fast construction layer; schema validation can remain a separate layer.\n\n## Benchmarks\n\nRun:\n\n```sh\nMIX_ENV=dev mix run bench/program_facts_like.exs\n```\n\nMachine used for this snapshot: Apple M5, Elixir 1.20, Erlang/OTP 29. Payload: `142 KB`, 250 nested `data_flow` records.\n\n| Case | ips | avg | memory |\n|---|---:|---:|---:|\n| `JSONCodec` map→struct | 4119.81 | 0.24 ms | 0.35 MB |\n| handwritten map→struct | 4009.64 | 0.25 ms | 0.25 MB |\n| `Jason.decode` only | 1378.28 | 0.73 ms | 1.10 MB |\n| `Spectral` pre-decoded | 1252.96 | 0.80 ms | 3.23 MB |\n| handwritten `Jason`+struct | 980.43 | 1.02 ms | 1.34 MB |\n| `JSONCodec` `Jason`+struct | 972.52 | 1.03 ms | 1.45 MB |\n| `Spectral` native JSON | 654.31 | 1.53 ms | 4.06 MB |\n\nInterpretation:\n\n- With `fast_path: :json`, `JSONCodec` is roughly tied with this handwritten decoder on decoded JSON maps, while still providing a generic fallback path.\n- End-to-end, JSON parsing dominates. `JSONCodec.decode!/1` is within ~1.01× of handwritten `Jason`+struct and ~1.49× faster than `Spectral` native JSON on this shape.\n- On map-heavy Iconify-like data (`mix run bench/iconify_like.exs`), `values_source:` avoids recomputing inherited defaults for every map entry. For advanced map-heavy decoders, `decode_values:` can return the final decoded map value directly when a custom decoder is clearer or faster than transforming a raw map and then invoking the generated nested decoder; in the Iconify-like benchmark this brings `JSONCodec` close to handwritten allocation.\n- The goal is not to beat perfect handwritten code on every shape immediately; it is to make the generated path close enough that hand-written decoders disappear.\n\n## Installation\n\n```elixir\n{:json_codec, \"~\u003e 0.1.1\"}\n```\n\n## Development\n\nSee [CHANGELOG.md](CHANGELOG.md) for release notes.\n\nThis project was bootstrapped with VibeKit conventions.\n\n```sh\nmix deps.get\nmix test\nmix ci\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fjson_codec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdannote%2Fjson_codec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdannote%2Fjson_codec/lists"}