{"id":13809305,"url":"https://github.com/martinthenth/goal","last_synced_at":"2025-05-16T12:08:10.866Z","repository":{"id":59701758,"uuid":"525487511","full_name":"martinthenth/goal","owner":"martinthenth","description":"A parameter validation library - based on Ecto","archived":false,"fork":false,"pushed_at":"2025-04-18T23:22:31.000Z","size":186,"stargazers_count":81,"open_issues_count":8,"forks_count":11,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-09T09:47:37.857Z","etag":null,"topics":["ecto","elixir","json","liveview","phoenix","phoenix-framework","phoenix-liveview","validation"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/goal","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/martinthenth.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":"2022-08-16T17:54:09.000Z","updated_at":"2025-03-26T11:29:15.000Z","dependencies_parsed_at":"2023-12-21T15:18:07.766Z","dependency_job_id":"eef625b0-1188-4948-82ec-7f4bef7059dd","html_url":"https://github.com/martinthenth/goal","commit_stats":{"total_commits":88,"total_committers":3,"mean_commits":"29.333333333333332","dds":0.3522727272727273,"last_synced_commit":"2b376a92dbf6b02d63527e3d7e3d158eccbafa31"},"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinthenth%2Fgoal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinthenth%2Fgoal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinthenth%2Fgoal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/martinthenth%2Fgoal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/martinthenth","download_url":"https://codeload.github.com/martinthenth/goal/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254527087,"owners_count":22085918,"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","json","liveview","phoenix","phoenix-framework","phoenix-liveview","validation"],"created_at":"2024-08-04T01:02:16.694Z","updated_at":"2025-05-16T12:08:10.827Z","avatar_url":"https://github.com/martinthenth.png","language":"Elixir","funding_links":[],"categories":["Validations"],"sub_categories":[],"readme":"# Goal\n\n[![CI](https://github.com/martinthenth/goal/actions/workflows/elixir.yml/badge.svg)](https://github.com/martinthenth/goal/actions/workflows/elixir.yml)\n[![Hex.pm](https://img.shields.io/hexpm/v/goal)](https://hex.pm/packages/goal)\n[![Hex.pm](https://img.shields.io/hexpm/dt/goal)](https://hex.pm/packages/goal)\n[![Hex.pm](https://img.shields.io/hexpm/l/goal)](https://github.com/martinthenth/goal/blob/main/LICENSE)\n\nGoal is a parameter validation library based on [Ecto](https://github.com/elixir-ecto/ecto).\nIt can be used with JSON APIs, HTML controllers and LiveViews.\n\nGoal builds a changeset from a validation schema and controller or LiveView parameters, and\nreturns the validated parameters or `Ecto.Changeset`, depending on the function you use.\n\nIf your frontend and backend use different parameter cases, you can recase parameter keys with\nthe `:recase_keys` option. `PascalCase`, `camelCase`, `kebab-case` and `snake_case` are\nsupported.\n\nYou can configure your own regexes for password, email, and URL format validations. This is\nhelpful in case of backward compatibility, where Goal's defaults might not match your production\nsystem's behavior.\n\n## Installation\n\nAdd `goal` to the list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [{:goal, \"~\u003e 1.1\"}]\nend\n```\n\n## Examples\n\nGoal can be used with LiveViews and JSON and HTML controllers.\n\n### Example with JSON and HTTP controllers\n\nWith JSON and HTML-based APIs, Goal takes the `params` from a controller action, validates those\nagainst a validation schema using `validate/3`, and returns an atom-based map or an error\nchangeset.\n\n```elixir\ndefmodule AppWeb.SomeController do\n  use AppWeb, :controller\n  use Goal\n\n  def create(conn, params) do\n    with {:ok, attrs} \u003c- validate(:create, params)) do\n      ...\n    else\n      {:error, changeset} -\u003e {:error, changeset}\n    end\n  end\n\n  defparams :create do\n    required :uuid, :string, format: :uuid\n    required :name, :string, min: 3, max: 3\n    optional :age, :integer, min: 0, max: 120\n    optional :gender, :enum, values: [\"female\", \"male\", \"non-binary\"]\n    optional :hobbies, {:array, :string}, max: 3, rules: [trim: true, min: 1]\n\n    optional :data, :map do\n      required :color, :string\n      optional :money, :decimal\n      optional :height, :float\n    end\n  end\nend\n```\n\n### Example with LiveViews\n\nWith LiveViews, Goal builds a changeset in `mount/3` that is assigned in the socket, and then it\ntakes the `params` from `handle_event/3`, validates those against a validation schema, and\nreturns an atom-based map or an error changeset.\n\n```elixir\ndefmodule AppWeb.SomeLiveView do\n  use AppWeb, :live_view\n  use Goal\n\n  def mount(params, _session, socket) do\n    changeset = changeset(:new, %{})\n    socket = assign(socket, :changeset, changeset)\n\n    {:ok, socket}\n  end\n\n  def handle_event(\"validate\", %{\"some\" =\u003e params}, socket) do\n    changeset = changeset(:new, params)\n    socket = assign(socket, :changeset, changeset)\n\n    {:noreply, socket}\n  end\n\n  def handle_event(\"save\", %{\"some\" =\u003e params}, socket) do\n    with {:ok, attrs} \u003c- validate(:new, params)) do\n      ...\n    else\n      {:error, changeset} -\u003e {:noreply, assign(socket, :changeset, changeset)}\n    end\n  end\n\n  defparams :new do\n    required :uuid, :string, format: :uuid\n    required :name, :string, min: 3, max: 3\n    optional :age, :integer, min: 0, max: 120\n    optional :gender, :enum, values: [\"female\", \"male\", \"non-binary\"]\n    optional :hobbies, {:array, :string}, max: 3, rules: [trim: true, min: 1]\n\n    optional :data, :map do\n      required :color, :string\n      optional :money, :decimal\n      optional :height, :float\n    end\n  end\nend\n```\n\n### Example with GraphQL resolvers\n\nWith GraphQL, you may want to validate input fields without marking them as `non-null` to enhance\nbackward compatibility. You can use Goal inside GraphQL resolvers to validate the input fields:\n\n```elixir\ndefmodule AppWeb.MyResolver do\n  use Goal\n\n  defparams(:create_user) do\n    required(:id, :uuid)\n    required(:input, :map) do\n      required(:first_name, :string)\n      required(:last_name, :string)\n    end\n  end\n\n  def create_user(args, info) do\n    with {:ok, attrs} \u003c- validate(:create_user) do\n      ...\n    end\n  end\nend\n```\n\n### Example with isolated schemas\n\nValidation schemas can be defined in a separate namespace, for example `AppWeb.MySchema`:\n\n```elixir\ndefmodule AppWeb.MySchema do\n  use Goal\n\n  defparams :show do\n    required :id, :string, format: :uuid\n    optional :query, :string\n  end\nend\n\ndefmodule AppWeb.SomeController do\n  use AppWeb, :controller\n\n  alias AppWeb.MySchema\n\n  def show(conn, params) do\n    with {:ok, attrs} \u003c- MySchema.validate(:show, params) do\n      ...\n    else\n      {:error, changeset} -\u003e {:error, changeset}\n    end\n  end\nend\n```\n\n## Features\n\n### Presence checks\n\nSometimes all you need is to check if a parameter is present:\n\n```elixir\nuse Goal\n\ndefparams :show do\n  required :id\n  optional :query\nend\n```\n\n### Deeply nested maps\n\nGoal efficiently builds error changesets for nested maps, and has support for lists of nested\nmaps. There is no limitation on depth.\n\n```elixir\nuse Goal\n\ndefparams :show do\n  optional :nested_map, :map do\n    required :id, :integer\n    optional :inner_map, :map do\n      required :id, :integer\n      optional :map, :map do\n        required :id, :integer\n        optional :list, {:array, :integer}\n      end\n    end\n  end\nend\n\niex(1)\u003e validate(:show, params)\n{:ok, %{nested_map: %{inner_map: %{map: %{id: 123, list: [1, 2, 3]}}}}}\n```\n\n### Powerful array validations\n\nIf you need expressive validations for arrays types, look no further!\n\nArrays can be made optional/required or the number of items can be set via `min`, `max` and `is`.\nAdditionally, `rules` allows specifying any validations that are available for the inner type.\nOf course, both can be combined:\n\n```elixir\nuse Goal\n\ndefparams do\n  required :my_list, {:array, :string}, max: 2, rules: [trim: true, min: 1]\nend\n\niex(1)\u003e Goal.validate_params(schema(), %{\"my_list\" =\u003e [\"hello \", \" world \"]})\n{:ok, %{my_list: [\"hello\", \"world\"]}}\n```\n\n### Readable error messages\n\nUse `Goal.traverse_errors/2` to build readable errors. Phoenix by default uses\n`Ecto.Changeset.traverse_errors/2`, which works for embedded Ecto schemas but not for the plain\nnested maps used by Goal. Goal's `traverse_errors/2` is compatible with (embedded)\n`Ecto.Schema`, so you don't have to make any changes to your existing logic.\n\n```elixir\ndef translate_errors(changeset) do\n  Goal.traverse_errors(changeset, \u0026translate_error/1)\nend\n```\n\n### Recasing inbound keys\n\nBy default, Goal will look for the keys defined in `defparams`. But sometimes frontend applications\nsend parameters in a different format. For example, in `camelCase` but your backend uses\n`snake_case`. For this scenario, Goal has the `:recase_keys` option:\n\n```elixir\nconfig :goal,\n  recase_keys: [from: :camel_case]\n\niex(1)\u003e MySchema.validate(:show, %{\"firstName\" =\u003e \"Jane\"})\n{:ok, %{first_name: \"Jane\"}}\n```\n\n### Recasing outbound keys\n\nUse `recase_keys/2` to recase outbound keys. For example, in your views:\n\n```elixir\nconfig :goal,\n  recase_keys: [to: :camel_case]\n\ndefmodule AppWeb.UserJSON do\n  import Goal\n\n  def show(%{user: user}) do\n    recase_keys(%{data: %{first_name: user.first_name}})\n  end\n\n  def error(%{changeset: changeset}) do\n    recase_keys(%{errors: Goal.Changeset.traverse_errors(changeset, \u0026translate_error/1)})\n  end\nend\n\niex(1)\u003e UserJSON.show(%{user: %{first_name: \"Jane\"}})\n%{data: %{firstName: \"Jane\"}}\niex(2)\u003e UserJSON.error(%Ecto.Changeset{errors: [first_name: {\"can't be blank\", [validation: :required]}]})\n%{errors: %{firstName: [\"can't be blank\"]}}\n```\n\n### Bring your own regex\n\nGoal has sensible defaults for string format validation. If you'd like to use your own regex,\ne.g. for validating email addresses or passwords, then you can add your own regex in the\nconfiguration:\n\n```elixir\nconfig :goal,\n  uuid_regex: ~r/^[[:alpha:]]+$/,\n  email_regex: ~r/^[[:alpha:]]+$/,\n  password_regex: ~r/^[[:alpha:]]+$/,\n  url_regex: ~r/^[[:alpha:]]+$/\n```\n\n### Available validations\n\nThe field types and available validations are:\n\n| Field type             | Validations                 | Description                                                                                          |\n| ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------- |\n| `:uuid`                | `:equals`                   | string value                                                                                         |\n| `:string`              | `:equals`                   | string value                                                                                         |\n|                        | `:is`                       | exact string length                                                                                  |\n|                        | `:min`                      | minimum string length                                                                                |\n|                        | `:max`                      | maximum string length                                                                                |\n|                        | `:trim`                     | boolean to remove leading and trailing spaces                                                         |\n|                        | `:squish`                   | boolean to trim and collapse spaces                                                                  |\n|                        | `:format`                   | `:uuid`, `:email`, `:password`, `:url`                                                               |\n|                        | `:subset`                   | list of required strings                                                                             |\n|                        | `:included`                 | list of allowed strings                                                                              |\n|                        | `:excluded`                 | list of disallowed strings                                                                           |\n| `:integer`             | `:equals`                   | integer value                                                                                        |\n|                        | `:is`                       | integer value                                                                                        |\n|                        | `:min`                      | minimum integer value                                                                                |\n|                        | `:max`                      | maximum integer value                                                                                |\n|                        | `:greater_than`             | minimum integer value                                                                                |\n|                        | `:less_than`                | maximum integer value                                                                                |\n|                        | `:greater_than_or_equal_to` | minimum integer value                                                                                |\n|                        | `:less_than_or_equal_to`    | maximum integer value                                                                                |\n|                        | `:equal_to`                 | integer value                                                                                        |\n|                        | `:not_equal_to`             | integer value                                                                                        |\n|                        | `:subset`                   | list of required integers                                                                            |\n|                        | `:included`                 | list of allowed integers                                                                             |\n|                        | `:excluded`                 | list of disallowed integers                                                                          |\n| `:float`               |                             | all of the integer validations                                                                       |\n| `:decimal`             |                             | all of the integer validations                                                                       |\n| `:boolean`             | `:equals`                   | boolean value                                                                                        |\n| `:date`                | `:equals`                   | date value                                                                                           |\n| `:time`                | `:equals`                   | time value                                                                                           |\n| `:enum`                | `:values`                   | list of allowed values                                                                               |\n| `:map`                 | `:properties`               | use `:properties` to define the fields                                                               |\n| `{:array, :map}`       | `:properties`               | use `:properties` to define the fields                                                               |\n| `{:array, inner_type}` | `:rules`                    | `inner_type` can be any basic type. `rules` supported all validations available for `inner_type`     |\n|                        | `:min`                      | minimum array length                                                                                 |\n|                        | `:max`                      | maximum array length                                                                                 |\n|                        | `:is`                       | exact array length                                                                                   |\n| More basic types       |                             | See [Ecto.Schema](https://hexdocs.pm/ecto/Ecto.Schema.html#module-primitive-types) for the full list |\n\nAll field types, excluding `:map` and `{:array, :map}`, can use `:equals`, `:subset`,\n`:included`, `:excluded` validations.\n\n## Benchmarks\n\nRun `mix deps.get` and then `mix run scripts/bench.exs` to run the benchmark on your computer.\n\n```zsh\nOperating System: macOS\nCPU Information: Apple M2 Pro\nNumber of Available Cores: 10\nAvailable memory: 16 GB\nElixir 1.16.2\nErlang 26.2.1\nJIT enabled: true\n\nBenchmark suite executing with the following configuration:\nwarmup: 5 s\ntime: 10 s\nmemory time: 5 s\nreduction time: 0 ns\nparallel: 1\ninputs: none specified\nEstimated total run time: 1 min 40 s\n\nName                                       ips        average  deviation         median         99th %\npresence params (4 fields)            702.67 K        1.42 μs  ±1370.44%        1.29 μs        1.63 μs\nsimple params (4 fields)              339.92 K        2.94 μs   ±367.42%        2.67 μs        4.96 μs\nflat params (12 fields)               115.59 K        8.65 μs    ±79.41%        8.04 μs       21.08 μs\nnested params (12 fields)             110.47 K        9.05 μs    ±88.77%        8.38 μs       39.88 μs\ndeeply nested params (12 fields)      107.88 K        9.27 μs    ±85.37%        8.33 μs       40.58 μs\n\nComparison:\npresence params (4 fields)            702.67 K\nsimple params (4 fields)              339.92 K - 2.07x slower +1.52 μs\nflat params (12 fields)               115.59 K - 6.08x slower +7.23 μs\nnested params (12 fields)             110.47 K - 6.36x slower +7.63 μs\ndeeply nested params (12 fields)      107.88 K - 6.51x slower +7.85 μs\n\nMemory usage statistics:\n\nName                                Memory usage\npresence params (4 fields)               4.76 KB\nsimple params (4 fields)                 7.95 KB - 1.67x memory usage +3.19 KB\nflat params (12 fields)                 25.36 KB - 5.33x memory usage +20.60 KB\nnested params (12 fields)               27.49 KB - 5.78x memory usage +22.73 KB\ndeeply nested params (12 fields)        27.38 KB - 5.75x memory usage +22.62 KB\n\n**All measurements for memory usage were the same**\n```\n\n## Credits\n\nThis library is based on [Ecto](https://github.com/elixir-ecto/ecto) and I had to copy and adapt\n`Ecto.Changeset.traverse_errors/2`. Thanks for making such an awesome library! 🙇\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartinthenth%2Fgoal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartinthenth%2Fgoal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartinthenth%2Fgoal/lists"}