{"id":24977700,"url":"https://github.com/adzz/ecto_morph","last_synced_at":"2025-10-27T23:12:21.124Z","repository":{"id":34639637,"uuid":"180820836","full_name":"Adzz/ecto_morph","owner":"Adzz","description":" morph your Ecto capabilities into the s t r a t o s p h e r e !","archived":false,"fork":false,"pushed_at":"2023-06-09T08:59:09.000Z","size":146,"stargazers_count":110,"open_issues_count":2,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-29T18:08:40.423Z","etag":null,"topics":["ecto","elixir","elixir-phoenix","functional-programming","phoenix"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Adzz.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}},"created_at":"2019-04-11T15:26:55.000Z","updated_at":"2024-12-20T13:35:08.000Z","dependencies_parsed_at":"2023-01-15T08:14:31.844Z","dependency_job_id":null,"html_url":"https://github.com/Adzz/ecto_morph","commit_stats":{"total_commits":116,"total_committers":8,"mean_commits":14.5,"dds":0.5258620689655172,"last_synced_commit":"ad5b3fb6ab6af7632417285e82dd4912b0cedc23"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fecto_morph","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fecto_morph/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fecto_morph/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Adzz%2Fecto_morph/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Adzz","download_url":"https://codeload.github.com/Adzz/ecto_morph/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247386265,"owners_count":20930619,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["ecto","elixir","elixir-phoenix","functional-programming","phoenix"],"created_at":"2025-02-03T23:08:47.204Z","updated_at":"2025-10-27T23:12:16.095Z","avatar_url":"https://github.com/Adzz.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# EctoMorph\n\n[![Module Version](https://img.shields.io/hexpm/v/ecto_morph.svg)](https://hex.pm/packages/ecto_morph)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/ecto_morph/)\n[![Total Download](https://img.shields.io/hexpm/dt/ecto_morph.svg)](https://hex.pm/packages/ecto_morph)\n[![License](https://img.shields.io/hexpm/l/ecto_morph.svg)](https://github.com/Adzz/ecto_morph/blob/master/LICENSE.md)\n[![Last Updated](https://img.shields.io/github/last-commit/Adzz/ecto_morph.svg)](https://github.com/Adzz/ecto_morph/commits/master)\n\nEctoMorph morphs your Ecto capabilities into the s t r a t o s p h e r e !\n\nParse incoming data into custom structs, then validate it.\n\nUsually you have to do something like this:\n\n```elixir\ndefmodule Embed do\n  use Ecto.Schema\n\n  embedded_schema do\n    field(:bar, :string)\n  end\nend\n\ndefmodule Test do\n  use Ecto.Schema\n\n  embedded_schema do\n    field(:thing, :string)\n    embeds_one(:embed, Embed)\n  end\n\nEcto.Changeset.cast(%Test{}, %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}, [:thing])\n|\u003e Ecto.Changeset.cast_embed(:embed)\n```\n\nNow we can do this:\n\n```elixir\ndata = %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, Test)\n\n# or\ndata = %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])\n\n# The data can also be a struct so this would work:\ndata = %Test{thing: \"foo\", embed: %Embed{bar: \"baz\"}}\nEctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])\n\n# So would this:\ndata = %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, %Test{}, [:thing, embed: [:bar]])\n\n# Changes can even be a different struct, if it\n# has overlapping keys they will be casted as expected:\n\ndefmoule OtherStruct do\n  defstruct [:thing, :embed]\nend\n\ndata = %OtherStruct{thing: \"foo\", embed: %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, %Test{}, [:thing, embed: [:bar]])\n```\n\nOr something like this:\n\n```elixir\nwith {:ok, %{status: 200, body: body}} \u003c- HTTPoison.get(\"mygreatapi.co.uk\") do\n  EctoMorph.cast_to_struct(Jason.decode!(body), User)\nend\n```\n\nWe can also whitelist fields to cast / update:\n\n```elixir\ndata = %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, Test, [:thing])\n\ndata = %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\nEctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])\n```\n\nSometimes it makes sense to update a struct we have retrieved from the database with data from our response. We can do that like so:\n\n```elixir\ndef update(data) do\n  # This will update the db struct with the data passed in, then update the db.\n  MyRepo.get!(MySchema, 10)\n  |\u003e EctoMorph.update_struct(data)\n  |\u003e MyRepo.update!()\nend\n```\n\n### Validations\n\nOften you'll want to do some validations, that's easy:\n\n```elixir\n(\n  %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\n  |\u003e EctoMorph.generate_changeset(Test, [:thing])\n  |\u003e Ecto.Changeset.validate_required([:thing])\n  |\u003e EctoMorph.into_struct()\n)\n\n# or\n(\n  %{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\n  |\u003e EctoMorph.generate_changeset(Test, [:thing])\n  |\u003e Ecto.Changeset.validate_change(...)\n  |\u003e Repo.insert!\n)\n```\n\n### Valiating Nested Changesets\n\nEasily the coolest feature, say you have nested changesets via embeds or has_one/many, you can now specify a path to a changeset and specify a validation function for the changeset(s) at the end of that path. If your path ends at a list of changesets (because your model has a has_many relation for example), each of those changesets will be validated.\n\n```elixir\n%{\"thing\" =\u003e \"foo\", \"embed\" =\u003e %{\"bar\"=\u003e \"baz\"}}\n|\u003e EctoMorph.generate_changeset(Test)\n|\u003e EctoMorph.validate_nested_changeset([:embed], \u0026MyEmbed.validate/1)\n\n# or\njson = %{\n  \"has_many\" =\u003e [\n    %{\"steamed_hams\" =\u003e [%{\"pickles\" =\u003e 1}, %{\"pickles\" =\u003e 2}]},\n    %{\"steamed_hams\" =\u003e [%{\"pickles\" =\u003e 1}]},\n    %{\"steamed_hams\" =\u003e [%{\"pickles\" =\u003e 4}, %{\"pickles\" =\u003e 5}]}\n  ]\n}\n\n# Here each of the steamed_hams above will have their pickle count validated:\n\nEctoMorph.generate_changeset(json, MySchema)\n|\u003e EctoMorph.validate_nested_changeset([:has_many, :steamed_hams], fn changeset -\u003e\n  Ecto.Changeset.validate_number(changeset, :pickles, greater_than: 3)\nend)\n```\n\n\nOther abilities include creating a map from an ecto struct, dropping optional fields if you decide to:\n\n```elixir\nEctoMorph.map_from_struct(%Test{})\n%{foo: \"bar\", updated_at: ~N[2000-01-01 23:00:07], inserted_at: ~N[2000-01-01 23:00:07], id: 10}\n\nEctoMorph.map_from_struct(%Test{}, [:exclude_timestamps])\n%{foo: \"bar\", id: 10}\n\nEctoMorph.map_from_struct(%Test{}, [:exclude_timestamps, :exclude_id])\n%{foo: \"bar\"}\n```\n\nand being able to filter some data by the fields in the given schema:\n\n```elixir\ndefmodule Test do\n  use Ecto.Schema\n\n  embedded_schema do\n    field(:random, :string)\n  end\nend\n\nEctoMorph.filter_by_schema_fields(%{random: \"data\", more: \"fields\"}, Test)\n%{random: \"data\"}\n```\n\nYou can even deep filter:\n\n```elixir\ndefmodule OtherThing do\n  use Ecto.Schema\n  @primary_key false\n  embedded_schema do\n    field(:id, :integer)\n  end\nend\n\ndefmodule Test do\n  use Ecto.Schema\n\n  embedded_schema do\n    field(:random, :string)\n    embeds_one(:other_thing, OtherThing)\n  end\nend\n\ndata = %{\n  random: \"data\",\n  more: \"fields\",\n  __meta__: \"stuff\",\n  other_thing: %{id: 1, ignored: \"field\"}\n}\n\nEctoMorph.deep_filter_by_schema_fields(data, Test)\n%{random: \"data\", other_thing: %{id: 1}}\n```\n\nDeep filtering will keep virtual fields, relations and through relations. That means you can use it to create a map of the struct fields without ecto metadata if you filter it by itself:\n\n```elixir\ndata = %Test{\n  random: \"data\",\n  more: \"fields\",\n  __meta__: \"stuff\",\n  other_thing: %OtherThing{id: 1, ignored: \"field\"}\n}\n\nEctoMorph.deep_filter_by_schema_fields(data, Test)\n%{random: \"data\", other_thing: %{id: 1}}\n```\n\nCheck out the [docs](https://hexdocs.pm/ecto_morph) for more examples and specifics\n\n\n### Contributing\n\n**NB** Set the `MIX_ENV` to `:docs` when publishing the package. This will ensure that modules inside `test/support` wont get their documentation published with the library (as they are included in the :dev env).\n\n```sh\nMIX_ENV=docs mix hex.publish\n```\n\n\n## Installation\n\nThe package can be installed by adding `:ecto_morph` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:ecto_morph, \"~\u003e 0.1.29\"}\n  ]\nend\n```\n\nDocumentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)\nand published on [HexDocs](https://hexdocs.pm). Once published, the docs can\nbe found at [https://hexdocs.pm/ecto_morph](https://hexdocs.pm/ecto_morph).\n\n## Copyright and License\n\nCopyright (c) 2019 Adam Lancaster\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadzz%2Fecto_morph","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadzz%2Fecto_morph","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadzz%2Fecto_morph/lists"}