{"id":32174261,"url":"https://github.com/ivanrublev/nestru","last_synced_at":"2026-02-19T07:02:58.673Z","repository":{"id":54335626,"uuid":"426781486","full_name":"IvanRublev/Nestru","owner":"IvanRublev","description":null,"archived":false,"fork":false,"pushed_at":"2025-05-30T14:18:45.000Z","size":72,"stargazers_count":43,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-01T10:26:50.285Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/IvanRublev.png","metadata":{"files":{"readme":"README.md","changelog":null,"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}},"created_at":"2021-11-10T21:25:46.000Z","updated_at":"2025-07-18T14:41:58.000Z","dependencies_parsed_at":"2024-03-30T22:24:34.715Z","dependency_job_id":"7270729a-8cb4-42e7-a70c-ec9fbec0d1d5","html_url":"https://github.com/IvanRublev/Nestru","commit_stats":{"total_commits":10,"total_committers":1,"mean_commits":10.0,"dds":0.0,"last_synced_commit":"59a277308ec949a0207c07ce5c80836fedbbcd76"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/IvanRublev/Nestru","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IvanRublev%2FNestru","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IvanRublev%2FNestru/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IvanRublev%2FNestru/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IvanRublev%2FNestru/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/IvanRublev","download_url":"https://codeload.github.com/IvanRublev/Nestru/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/IvanRublev%2FNestru/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29605806,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T06:47:36.664Z","status":"ssl_error","status_checked_at":"2026-02-19T06:45:47.551Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":[],"created_at":"2025-10-21T19:01:14.113Z","updated_at":"2026-02-19T07:02:58.667Z","avatar_url":"https://github.com/IvanRublev.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nestru\n\n```elixir\nMix.install([:nestru], force: true, consolidate_protocols: false)\n```\n\n## About\n\n| [![Coverage Status](https://coveralls.io/repos/github/IvanRublev/Nestru/badge.svg)](https://coveralls.io/github/IvanRublev/Nestru) | [![hex.pm version](http://img.shields.io/hexpm/v/nestru.svg?style=flat)](https://hex.pm/packages/nestru) |\n| ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |\n\n🔗 Full documentation is on [hexdocs.pm](https://hexdocs.pm/nestru/)\n\n🔗 JSON parsing example is in [elixir-decode-validate-json-with-nestru-domo](https://github.com/IvanRublev/elixir-decode-validate-json-with-nestru-domo) repo.\n\n### Description\n\n\u003c!-- Documentation --\u003e\n\nA library to serialize between maps and nested structs.\n\nTurns a map into a nested struct according to hints given to the library.\nAnd vice versa turns any nested struct into a map.\n\nIt works with maps/structs of any shape and level of nesting. Highly configurable\nby implementing `Nestru.Decoder` and `Nestru.Encoder` protocols for structs.\n\nUseful for translating map keys to struct's fields named differently. \nOr to specify default values missing in the map and required by struct.\n\nThe library's primary purpose is to serialize a map coming from a JSON payload \nor an Erlang term; at the same time, the map can be of any origin.\n\nThe input map can have atom or binary keys. The library takes the binary key first \nand then the same-named atom key if the binary key is missing while decoding\nthe map.\nThe library generates maps with atom keys during the struct encode operation.\n\n## Tour\n\n\u003cp align=\"center\" class=\"hidden\"\u003e\n  \u003ca href=\"https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FIvanRublev%2FNestru%2Fblob%2Fmaster%2FREADME.md\"\u003e\n    \u003cimg src=\"https://livebook.dev/badge/v1/blue.svg\" alt=\"Run in Livebook\" /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\nLet's say we have an `Order` with a total field which is an instance of a `Total` struct.\nAnd we want to serialize between an instance of `Order` and a map.\n\nFirstly, let's derive `Nestru.Encoder` and `Nestru.Decoder` protocols \nand give a hint that the field `:total` should hold a value of `Total` struct\nlike the following:\n\n```elixir\ndefmodule Order do\n  @derive [Nestru.Encoder, {Nestru.Decoder, hint: %{total: Total}}]\n  defstruct [:id, :total]\nend\n\ndefmodule Total do\n  @derive [Nestru.Encoder, Nestru.Decoder]\n  defstruct [:sum]\nend\n```\n```output\n{:module, Total, \u003c\u003c70, 79, 82, 49, 0, 0, 8, ...\u003e\u003e, %Total{sum: nil}}\n```\n\nSecondly, we can encode the `Order` into the map like that:\n\n```elixir\nmodel = %Order{id: \"A548\", total: %Total{sum: 500}}\n{:ok, map} = Nestru.encode(model)\n```\n```output\n{:ok, %{id: \"A548\", total: %{sum: 500}}}\n```\n\nAnd decode the map back into the `Order` like the following:\n\n```elixir\nmap = %{\n  \"id\" =\u003e \"A548\",\n  \"total\" =\u003e %{\"sum\" =\u003e 500}\n}\n\n{:ok, model} = Nestru.decode(map, Order)\n```\n```output\n{:ok, %Order{id: \"A548\", total: %Total{sum: 500}}}\n```\n\nAs you can see the data markup is in place, the `Total` struct is nested within the `Order` struct.\n\n## A list of structs in a field\n\nLet's add the `:items` field to `Order1` struct to hold a list of `LineItem`s \nand give a hint to `Nestru` on how to decode that field:\n\n```elixir\ndefmodule Order1 do\n  @derive {Nestru.Decoder, hint: %{total: Total, items: [LineItem]}}\n\n  defstruct [:id, :items, :total]\nend\n\ndefmodule LineItem do\n  @derive Nestru.Decoder\n  defstruct [:amount]\nend\n```\n```output\n{:module, LineItem, \u003c\u003c70, 79, 82, 49, 0, 0, 8, ...\u003e\u003e, %LineItem{amount: nil}}\n```\n\nLet's decode:\n\n```elixir\nmap = %{\n  \"id\" =\u003e \"A548\",\n  \"items\" =\u003e [%{\"amount\" =\u003e 150}, %{\"amount\" =\u003e 350}],\n  \"total\" =\u003e %{\"sum\" =\u003e 500}\n}\n\n{:ok, model} = Nestru.decode(map, Order1)\n```\n```output\n{:ok,\n %Order1{\n   id: \"A548\",\n   items: [%LineItem{amount: 150}, %LineItem{amount: 350}],\n   total: %Total{sum: 500}\n }}\n```\n\nVoilà, we have field values as nested structs 🎉\n\nFor the case when the list contains several structs of different types, please,\nsee the Serializing type-dependent fields section below.\n\n\n## Date Time and URI\n\nLet's say we have an `Order2` struct with some `URI` and `DateTime` fields in it. \nThese attributes are structs in Elixir, at the same time they usually\nkept as binary representations in a map.\n\n`Nestru` supports conversion between binaries \nand structs, all we need to do is to implement the `Nestry.Encoder` \nand `Nestru.Decoder` protocols for these structs like the following:\n\n```elixir\n# DateTime\ndefimpl Nestru.Encoder, for: DateTime do\n  def gather_fields_from_struct(struct, _context) do\n    {:ok, DateTime.to_string(struct)}\n  end\nend\n\ndefimpl Nestru.Decoder, for: DateTime do\n  def decode_fields_hint(_empty_struct, _context, value) do\n    case DateTime.from_iso8601(value) do\n      {:ok, date_time, _offset} -\u003e {:ok, date_time}\n      error -\u003e error\n    end\n  end\nend\n\n# URI\ndefimpl Nestru.Encoder, for: URI do\n  def gather_fields_from_struct(struct, _context) do\n    {:ok, URI.to_string(struct)}\n  end\nend\n\ndefimpl Nestru.Decoder, for: URI do\n  def decode_fields_hint(_empty_struct, _context, value) do\n    URI.new(value)\n  end\nend\n```\n```output\n{:module, Nestru.Decoder.URI, \u003c\u003c70, 79, 82, 49, 0, 0, 8, ...\u003e\u003e, {:decode_fields_hint, 3}}\n```\n\n`Order2` is defined like this:\n\n```elixir\ndefmodule Order2 do\n  @derive [Nestru.Encoder, {Nestru.Decoder, hint: %{date: DateTime, website: URI}}]\n  defstruct [:id, :date, :website]\nend\n```\n```output\n{:module, Order2, \u003c\u003c70, 79, 82, 49, 0, 0, 8, ...\u003e\u003e, %Order2{id: nil, date: nil, website: nil}}\n```\n\nWe can encode it to a map with binary fields like the following:\n\n```elixir\norder = %Order2{id: \"B445\", date: ~U[2024-03-15 22:42:03Z], website: URI.parse(\"https://www.example.com/?book=branch\")}\n\n{:ok, map} = Nestru.encode(order)\n```\n```output\n{:ok, %{id: \"B445\", date: \"2024-03-15 22:42:03Z\", website: \"https://www.example.com/?book=branch\"}}\n```\n\nAnd decode it back:\n\n```elixir\nNestru.decode(map, Order2)\n```\n```output\n{:ok,\n %Order2{\n   id: \"B445\",\n   date: ~U[2024-03-15 22:42:03Z],\n   website: %URI{\n     scheme: \"https\",\n     userinfo: nil,\n     host: \"www.example.com\",\n     port: 443,\n     path: \"/\",\n     query: \"book=branch\",\n     fragment: nil\n   }\n }}\n```\n\n\n## Error handling and path to the failed part of the map\n\nEvery implemented function of Nestru protocols can return `{error, message}` tuple \nin case of failure. When `Nestru` receives the error tuple, it stops conversion\nand bypasses the error to the caller.\n\n```elixir\ndefmodule Location do\n  @derive {Nestru.Decoder, hint: %{street: Street}}\n  defstruct [:street]\nend\n\ndefmodule Street do\n  @derive {Nestru.Decoder, hint: %{house: House}}\n  defstruct [:house]\nend\n\ndefmodule House do\n  defstruct [:number]\n\n  defimpl Nestru.Decoder do\n    def decode_fields_hint(_empty_struct, _context, value) do\n      if Nestru.has_key?(value, :number) do\n        {:ok, %{}}\n      else\n        {:error, \"Can't continue without house number.\"}\n      end\n    end\n  end\nend\n```\n\nSo when we decode the following map missing the `number` value, we will get\nthe error back:\n\n```elixir\nmap = %{\n  \"street\" =\u003e %{\n    \"house\" =\u003e %{\n      \"name\" =\u003e \"Party house\"\n    }\n  }\n}\n\n{:error, error} = Nestru.decode(map, Location)\n```\n```output\n{:error,\n %{\n   get_in_keys: [#Function\u003c8.67001686/3 in Access.key!/1\u003e, #Function\u003c8.67001686/3 in Access.key!/1\u003e],\n   message: \"Can't continue without house number.\",\n   path: [\"street\", \"house\"]\n }}\n```\n\n`Nestru` wraps the error message into a map and adds `path` and `get_in_keys`\nfields to it. The path values point to the failed part of the map which can\nbe returned like the following:\n\n```elixir\nget_in(map, error.get_in_keys)\n```\n```output\n%{\"name\" =\u003e \"Party house\"}\n```\n\n## Maps with different key names\n\nIn some cases, the map's keys have slightly different names compared \nto the target's struct field names. Fields that should be decoded into the struct \ncan be gathered by adopting `Nestru.PreDecoder` protocol like the following:\n\n```elixir\ndefmodule Quote do\n  @derive [\n    {Nestru.PreDecoder, translate: %{\"cost_value\" =\u003e :cost}},\n    Nestru.Decoder\n  ]\n\n  defstruct [:cost]\nend\n```\n\nWhen we decode the map, `Nestru` will put the value of the `\"cost_value\"` key\nfor the `:cost` key into the map and then complete the decoding:\n\n```elixir\nmap = %{\n  \"cost_value\" =\u003e 1280\n}\n\nNestru.decode(map, Quote)\n```\n```output\n{:ok, %Quote{cost: 1280}}\n```\n\nFor more sophisticated key mapping you can implement \nthe `gather_fields_for_decoding/3` function of `Nestru.PreDecoder` explicitly.\n\n## Serializing type-dependent fields\n\nTo convert a struct with a field that can have the value of multiple struct types\ninto the map and back, the type of the field's value should be persisted. \nIt's possible to do that like the following:\n\n```elixir\ndefmodule BookCollection do\n  defstruct [:name, :items]\n\n  defimpl Nestru.Encoder do\n    def gather_fields_from_struct(struct, _context) do\n      items_kinds =\n        Enum.map(struct.items, fn %module{} -\u003e\n          module\n          |\u003e Module.split()\n          |\u003e Enum.join(\".\")\n        end)\n\n      {:ok, %{name: struct.name, items: struct.items, items_kinds: items_kinds}}\n    end\n  end\n\n  defimpl Nestru.Decoder do\n    def decode_fields_hint(_empty_struct, _context, value) do\n      items_kinds =\n        Enum.map(value.items_kinds, fn module_string -\u003e\n          module_string\n          |\u003e String.split(\".\")\n          |\u003e Module.safe_concat()\n        end)\n\n      {:ok, %{items: \u0026Nestru.decode_from_list(\u00261, items_kinds)}}\n    end\n  end\nend\n\ndefmodule BookCollection.Book do\n  @derive [Nestru.Encoder, Nestru.Decoder]\n  defstruct [:title]\nend\n\ndefmodule BookCollection.Magazine do\n  @derive [Nestru.Encoder, Nestru.Decoder]\n  defstruct [:issue]\nend\n```\n\nLet's convert the nested struct into a map. The returned map gets \nextra `items_kinds` field with types information:\n\n```elixir\nalias BookCollection.{Book, Magazine}\n\ncollection = %BookCollection{\n  name: \"Duke of Norfolk's archive\",\n  items: [\n    %Book{title: \"The Spell in the Chasm\"},\n    %Magazine{issue: \"Strange Hunt\"}\n  ]\n}\n\n{:ok, map} = Nestru.encode(collection)\n```\n```output\n{:ok,\n %{\n   items: [%{title: \"The Spell in the Chasm\"}, %{issue: \"Strange Hunt\"}],\n   items_kinds: [\"BookCollection.Book\", \"BookCollection.Magazine\"],\n   name: \"Duke of Norfolk's archive\"\n }}\n```\n\nAnd restoring of the original nested struct is as simple as that:\n\n```elixir\n{:ok, collection} = Nestru.decode(map, BookCollection)\n```\n```output\n{:ok,\n %BookCollection{\n   items: [\n     %BookCollection.Book{title: \"The Spell in the Chasm\"},\n     %BookCollection.Magazine{issue: \"Strange Hunt\"}\n   ],\n   name: \"Duke of Norfolk's archive\"\n }}\n```\n\n## Use with other libraries\n\n### Jason\n\nJSON maps decoded with [Jason library](https://github.com/michalmuskala/jason/) \nare supported with both binary and atoms keys.\n\n### ex_json_schema\n\n[ex_json_schema library](https://hex.pm/packages/ex_json_schema) can be used \nbefore decoding the input map with the JSON schema. To make sure that \nthe structure of the input map is correct.\n\n### ExJSONPath\n\n[ExJsonPath library](https://hex.pm/packages/exjsonpath) allows querying maps\n(JSON objects) and lists (JSON arrays), using JSONPath expressions.\nThe queries can be useful in `Nestru.PreDecoder.gather_fields_for_decoding/3`\nfunction to assemble fields for decoding from a map having a very different shape\nfrom the target struct.\n\n### Domo\n\nYou can use the [Domo library](https://github.com/IvanRublev/Domo) \nto validate the `t()` types of the nested struct values after \ndecoding with `Nestru`.\n\n`Domo` can validate a nested struct in one pass, ensuring that \nthe struct's field values match its `t()` type and associated preconditions.\n\n\u003c!-- Documentation --\u003e\n\n## Changelog\n\n### 1.0.1\n\n* Rename list functions `decode_from_list_of_maps` to `decode_from_list` and `encode_to_list_of_maps` to `encode_to_list`\n\n### 1.0.0\n\n* Convert structs to/from binaries for better serialization of `DateTime` and `URI` to/from strings\n* Breaking changes in function names:\n  * `Nestru.PreDecoder.gather_fields_from_map/3` has been renamed to `gather_fields_for_decoding/3`\n  * `Nestru.Decoder.from_map_hint/1` has been renamed to `Nestru.Decoder.decode_fields_hint/1`\n  * `Nestru.decode_from_map/3` has been renamed to `Nestru.decode/3`\n  * `Nestru.encode_to_map/2` has been renamed to `Nestru.encode/2`\n* The `Nestru.Decoder.decode_fields_hint/3` can now return a struct as a hint as `{:ok, %struct{}}`. \n  In this case `Nestru.decode/3` will return the struct as the decoded value.\n\n### 0.3.3\n\n* Fix the regress - make the decoding of an empty list return an empty list\n\n### 0.3.2\n\n* Return error from `decode_from_list_of_maps(!)/2/3` for non-list values\n\n### 0.3.1\n\n* Add `:only` and `:except` options for deriving of `Nestru.Encoder` protocol\n* Add explicit `:translate` option for deriving of `Nestru.PreDecoder` protocol\n* Add explicit `:hint` option for deriving of `Nestru.Decoder` protocol\n\n### 0.3.0\n\n* Rename `Nestru.PreDecoder.gather_fields_map/3` to `gather_fields_for_decoding/3`.\n* Rename `Nestru.Encoder.encode/1` to `Nestru.Encoder.gather_fields_from_struct/2`\n* Make `encode(!)/2` work only with structs and add `encode_to_list_of_maps(!)/2` for lists.\n* Add context parameter to `encode_to_*` functions.\n\n### 0.2.1\n\n* Fix `decode(!)/2/3` to return the error for not a map value.\n\n### 0.2.0\n\n* Fix to ensure the module is loaded before checking if it's a struct\n* Add `decode` and `encode` verbs to function names\n* Support `[Module]` hint in the map returned from `decode_fields_hint` to decode the list of structs\n* Support `%{one_key: :other_key}` mapping configuration for the `PreDecoder` protocol in `@derive` attribute.\n\n### 0.1.1\n\n* Add `has_key?/2` and `get/3` map functions that look up keys \n  both in a binary or an atom form.\n\n### 0.1.0\n\n* Initial release.\n\n## License\n\nCopyright © 2021 Ivan Rublev\n\nThis project is licensed under the [MIT license](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivanrublev%2Fnestru","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fivanrublev%2Fnestru","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fivanrublev%2Fnestru/lists"}