{"id":15713672,"url":"https://github.com/nicolab/crystal-validator","last_synced_at":"2025-05-12T20:56:38.857Z","repository":{"id":55122229,"uuid":"237091435","full_name":"Nicolab/crystal-validator","owner":"Nicolab","description":":gem: Data validation module for Crystal lang","archived":false,"fork":false,"pushed_at":"2021-06-19T09:52:22.000Z","size":810,"stargazers_count":30,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-04-01T03:34:55.958Z","etag":null,"topics":["crystal","crystal-lang","validation","validator"],"latest_commit_sha":null,"homepage":"https://nicolab.github.io/crystal-validator/","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Nicolab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"Nicolab","patreon":"nicolab","custom":"https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=PGRH4ZXP36GUC"}},"created_at":"2020-01-29T22:03:57.000Z","updated_at":"2024-11-03T15:59:42.000Z","dependencies_parsed_at":"2022-08-14T12:40:31.664Z","dependency_job_id":null,"html_url":"https://github.com/Nicolab/crystal-validator","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nicolab%2Fcrystal-validator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nicolab%2Fcrystal-validator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nicolab%2Fcrystal-validator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Nicolab%2Fcrystal-validator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Nicolab","download_url":"https://codeload.github.com/Nicolab/crystal-validator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246625557,"owners_count":20807772,"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":["crystal","crystal-lang","validation","validator"],"created_at":"2024-10-03T21:32:51.900Z","updated_at":"2025-04-01T10:32:45.407Z","avatar_url":"https://github.com/Nicolab.png","language":"Crystal","funding_links":["https://github.com/sponsors/Nicolab","https://patreon.com/nicolab","https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=PGRH4ZXP36GUC"],"categories":[],"sub_categories":[],"readme":"# validator\n\n[![CI Status](https://github.com/Nicolab/crystal-validator/workflows/CI/badge.svg?branch=master)](https://github.com/Nicolab/crystal-validator/actions) [![GitHub release](https://img.shields.io/github/release/Nicolab/crystal-validator.svg)](https://github.com/Nicolab/crystal-validator/releases) [![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](https://nicolab.github.io/crystal-validator/)\n\n∠(・.-)―〉 →◎ `validator` is a [Crystal](https://crystal-lang.org) data validation module.\u003cbr\u003e\nVery simple and efficient, all validations return `true` or `false`.\n\nAlso [validator/check](#check) (not exposed by default) provides:\n\n* Error message handling intended for the end user.\n* Also (optional) a powerful and productive system of validation rules.\n  With self-generated granular methods for cleaning and checking data.\n\n**Validator** respects the [KISS principle](https://en.wikipedia.org/wiki/KISS_principle) and the [Unix Philosophy](https://en.wikipedia.org/wiki/Unix_philosophy). It's a great basis tool for doing your own validation logic on top of it.\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n```yaml\ndependencies:\n  validator:\n    github: nicolab/crystal-validator\n```\n\n2. Run `shards install`\n\n## Usage\n\n* [Validator - API docs](https://nicolab.github.io/crystal-validator/)\n\nThere are 3 main ways to use *validator*:\n\n* As a simple validator to check rules (eg: email, url, min, max, presence, in, ...) which return a boolean.\n* As a more advanced validation system which will check a series of rules and returns all validation errors encountered with custom or standard messages.\n* As a system of validation rules (inspired by the _Laravel framework's Validator_)\n  which makes data cleaning and data validation in Crystal very easy!\n  With self-generated granular methods for cleaning and checking data of each field.\n\nBy default the **validator** module expose only `Validator` and `Valid` (alias) in the scope:\n\n```crystal\nrequire \"validator\"\n\nValid.email? \"contact@example.org\" # =\u003e true\nValid.url? \"https://github.com/Nicolab/crystal-validator\" # =\u003e true\nValid.my_validator? \"value to validate\", \"hello\", 42 # =\u003e true\n```\n\nAn (optional) expressive validation flavor, `is` available as an alternative.\nNot exposed by default, it must be imported:\n\n```crystal\nrequire \"validator/is\"\n\nis :email?, \"contact@example.org\" # =\u003e true\nis :url?, \"https://github.com/Nicolab/crystal-validator\" # =\u003e true\nis :my_validator?, \"value to validate\", \"hello\", 42 # =\u003e true\n\n\n# raises an error if the email is not valid\nis! :email?, \"contact@@example..org\" # =\u003e Validator::Error\n```\n\n`is` is a macro, no overhead during the runtime 🚀\n By the nature of the macros, you can't pass the *validator* name dynamically with a variable like that `is(validator_name, \"my value to validate\", arg)`.\n But of course you can pass arguments with variables `is(:validator_name?, arg1, arg2)`.\n\n* [Validator - API docs](https://nicolab.github.io/crystal-validator/)\n\n### Validation rules\n\nThe validation rules can be defined directly when defining properties (with `getter` or `property`).\nOr with the macro `Check.rules`. Depending on preference, it's the same under the hood.\n\n```crystal\nrequire \"validator/check\"\n\nclass User\n  # Mixin\n  Check.checkable\n\n  # required\n  property email : String, {\n    required: true,\n\n    # Optional lifecycle hook to be executed on `check_email` call.\n    # Before the `check` rules, just after `clean_email` called inside `check_email`.\n    # Proc or method name (here is a Proc)\n    before_check: -\u003e(v : Check::Validation, content : String?, required : Bool, format : Bool) {\n      puts \"before_check_content\"\n      content\n    },\n\n    # Optional lifecycle hook to be executed on `check_email` call, after the `check` rules.\n    # Proc or method name (here is the method name)\n    after_check: :after_check_email\n\n    # Checker (all validators are supported)\n    check: {\n      not_empty: {\"Email is required\"},\n      email:     {\"It is not a valid email\"},\n    },\n\n    # Cleaner\n    clean: {\n      # Data type\n      type: String,\n\n      # Converter (if union or other) to the expected value type.\n      # Example if the input value is i32, but i64 is expected\n      # Here is a String\n      to: :to_s,\n\n      # Formatter (any Crystal Proc) or method name (Symbol)\n      format: :format_email,\n\n      # Error message\n      # Default is \"Wrong type\" but it can be customized\n      message: \"Oops! Wrong type.\",\n    }\n  }\n\n  # required\n  property age : Int32, {\n    required: \"Age is required\", # Custom message\n    check: {\n      min:     {\"Age should be more than 18\", 18},\n      between: {\"Age should be between 25 and 35\", 25, 35},\n    },\n    clean: {type: Int32, to: :to_i32, message: \"Unable to cast to Int32\"},\n  }\n\n  # nilable\n  property bio : String?, {\n    check: {\n      between: {\"The user bio must be between 2 and 400 characters.\", 2, 400},\n    },\n    clean: {\n      type: String,\n      to: :to_s,\n      # `nilable` means omited if not provided,\n      # regardless of Crystal type (nilable or not)\n      nilable: true\n    },\n  }\n\n  def initialize(@email, @age)\n  end\n\n  # ---------------------------------------------------------------------------\n  # Lifecycle methods (hooks)\n  # ---------------------------------------------------------------------------\n\n  # Triggered on instance: `user.check`\n  private def before_check(v : Check::Validation, required : Bool, format : Bool)\n    # Code...\n  end\n\n  # Triggered on instance: `user.check`\n  private def after_check(v : Check::Validation, required : Bool, format : Bool)\n    # Code...\n  end\n\n  # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)\n  private def self.before_check(v : Check::Validation, h, required : Bool, format : Bool)\n    # Code...\n    pp h\n  end\n\n  # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)\n  private def self.after_check(v : Check::Validation, h, cleaned_h, required : Bool, format : Bool)\n    # Code...\n    pp cleaned_h\n    cleaned_h # \u003c= returns cleaned_h!\n  end\n\n  # Triggered on a static call and on instance call: `User.check_email(value)`, `User.check(h)`, `user.check`.\n  private def self.after_check_content(v : Check::Validation, content : String?, required : Bool, format : Bool)\n    puts \"after_check_content\"\n    puts \"Valid? #{v.valid?}\"\n    content\n  end\n\n  # --------------------------------------------------------------------------\n  #  Custom checkers\n  # --------------------------------------------------------------------------\n\n  # Triggered on instance: `user.check`\n  @[Check::Checker]\n  private def custom_checker(v : Check::Validation, required : Bool, format : Bool)\n    # Code...\n  end\n\n    # Triggered on a static call: `User.check(h)` (with a `Hash` or `JSON::Any`)\n  @[Check::Checker]\n  private def self.custom_checker(v : Check::Validation, h, cleaned_h, required : Bool, format : Bool)\n    # Code...\n    cleaned_h # \u003c= returns cleaned_h!\n  end\n\n  # --------------------------------------------------------------------------\n  #  Formatters\n  # --------------------------------------------------------------------------\n\n  # Format (convert) email.\n  def self.format_email(email)\n    puts \"mail stripped\"\n    email.strip\n  end\n\n  # --------------------------------------------------------------------------\n  # Normal methods\n  # --------------------------------------------------------------------------\n\n  def foo()\n    # Code...\n  end\n\n  def self.bar(v)\n    # Code...\n  end\n\n  # ...\nend\n```\n\n__Check__ with this example class (`User`):\n\n```crystal\n# Check a Hash (statically)\nv, user_h = User.check(input_h)\n\npp v # =\u003e Validation instance\npp v.valid?\npp v.errors\n\npp user_h # =\u003e Casted and cleaned Hash\n\n# Same but raise if there is a validation error\nuser_h = User.check!(input_h)\n\n# Check a Hash (on instance)\nuser = user.new(\"demo@example.org\", 38)\n\nv = user.check # =\u003e Validation instance\npp v.valid?\npp v.errors\n\n# Same but raise if there is a validation error\nuser.check! # =\u003e Validation instance\n\n# Example with an active record model\nuser.check!.save\n\n# Check field\nv, email = User.check_email(value: \"demo@example.org\")\nv, age = User.check_age(value: 42)\n\n# Same but raise if there is a validation error\nemail = User.check_email!(value: \"demo@example.org\")\n\nv, email = User.check_email(value: \"demo@example.org \", format: true)\nv, email = User.check_email(value: \"demo@example.org \", format: false)\n\n# Using an existing Validation instance\nv = Check.new_validation\nv, email = User.check_email(v, value: \"demo@example.org\")\n\n# Same but raise if there is a validation error\nemail = User.check_email!(v, value: \"demo@example.org\")\n```\n\n__Clean__ with this example class (`User`):\n\n```crystal\n# `check` method cleans all values of the Hash (or JSON::Any),\n# before executing the validation rules\nv, user_h = User.check(input_h)\n\npp v # =\u003e Validation instance\npp v.valid?\npp v.errors\n\npp user_h # =\u003e Casted and cleaned Hash\n\n# Cast and clean field\nok, email = User.clean_email(value: \"demo@example.org\")\nok, age = User.clean_age(value: 42)\n\nok, email = User.clean_email(value: \"demo@example.org \", format: true)\nok, email = User.clean_email(value: \"demo@example.org \", format: false)\n\nputs \"${email} is casted and cleaned\" if ok\n# or\nputs \"Email type error\" unless ok\n```\n\n* `clean_*` methods are useful to caste a union value (like `Hash` or `JSON::Any`).\n* Also `clean_*` methods are optional and handy for formatting values, such as the strip on the email in the example `User` class.\n\nMore details about cleaning, casting, formatting and return values:\n\nBy default `format` is `true`, to disable:\n\n```crystal\nok, email = User.clean_email(value: \"demo@example.org\", format: false)\n# or\nok, email = User.clean_email(\"demo@example.org\", false)\n```\n\nAlways use named argument if there is only one (the `value`):\n\n```crystal\nok, email = User.clean_email(value: \"demo@example.org\")\n```\n\n`ok` is a boolean value that reports whether the cast succeeded. Like the type assertions in _Go_ (lang).\nBut the `ok` value is returned in first (like in _Elixir_ lang) for easy handling of multiple return values (`Tuple`).\n\nExample with multiple values returned:\n\n```crystal\nok, value1, value2 = User.clean_my_tuple({1, 2, 3})\n\n# Same but raise if there is a validation error\nvalue1, value2 = User.clean_my_tuple!({1, 2, 3})\n```\n\nConsidering the example class above (`User`).\nAs a reminder, the email field has been defined with the formatter below:\n\n```crystal\nCheck.rules(\n  email: {\n    clean: {\n      type:    String,\n      to:      :to_s,\n      format:  -\u003eself.format_email(String), # \u003c= Here!\n      message: \"Wrong type\",\n    },\n  },\n)\n\n# ...\n\n# Format (convert) email.\ndef self.format_email(email)\n  puts \"mail stripped\"\n  email.strip\nend\n```\n\nSo `clean_email` cast to `String` and strip the value `\" demo@example.org \"`:\n\n```crystal\n# Email value with one space before and one space after\nok, email = User.clean_email(value: \" demo@example.org \")\n\nputs email # =\u003e \"demo@example.org\"\n\n# Same but raise if there is a validation error\n# Email value with one space before and one space after\nemail = User.clean_email!(value: \" demo@example.org \")\n\nputs email # =\u003e \"demo@example.org\"\n```\n\nIf the email was taken from a union type (`json[\"email\"]?`), the returned `email` variable would be a `String` too.\n\nSee [more examples](https://github.com/Nicolab/crystal-validator/tree/master/examples).\n\n\u003e NOTE: Require more explanations about `required`, `nilable` rules.\n\u003e Also about the converters JSON / Crystal Hash: `h_from_json`, `to_json_h`, `to_crystal_h`.\n\u003e In the meantime see the [API doc](https://nicolab.github.io/crystal-validator/Check/Checkable.html).\n\n### Validation#check\n\nTo perform a series of validations with error handling, the [validator/check](https://nicolab.github.io/crystal-validator/Check.html) module offers this possibility 👍\n\nA [Validation](https://nicolab.github.io/crystal-validator/Check/Validation.html) instance provides the means to write sequential checks, fine-tune each micro-validation with their own rules and custom error message, the possibility to retrieve all error messages, etc.\n\n\u003e `Validation` is also used with `Check.rules` and `Check.checkable`\n  that provide a powerful and productive system of validation rules\n  which makes data cleaning and data validation in Crystal very easy.\n  With self-generated granular methods for cleaning and checking data.\n\nTo use the checker (`check`) includes in the `Validation` class:\n\n```crystal\nrequire \"validator/check\"\n\n# Validates the *user* data received in the HTTP controller or other.\ndef validate_user(user : Hash) : Check::Validation\n  v = Check.new_validation\n\n  # -- email\n\n  # Hash key can be a String or a Symbol\n  v.check :email, \"The email is required.\", is :presence?, :email, user\n\n  v.check \"email\", \"The email is required.\", is :presence?, \"email\", user\n  v.check \"email\", \"#{user[\"email\"]} is an invalid email.\", is :email?, user[\"email\"]\n\n  # -- username\n\n  v.check \"username\", \"The username is required.\", is :presence?, \"username\", user\n\n  v.check(\n    \"username\",\n    \"The username must contain at least 2 characters.\",\n    is :min?, user[\"username\"], 2\n  )\n\n  v.check(\n    \"username\",\n    \"The username must contain a maximum of 20 characters.\",\n    is :max?, user[\"username\"], 20\n  )\nend\n\nv = validate_user user\n\npp v.valid? # =\u003e true (or false)\n\n# Inverse of v.valid?\nif v.errors.empty?\n  return \"no error\"\nend\n\n# Print all the errors (if any)\npp v.errors\n\n# It's a Hash of Array\nerrors = v.errors\n\nputs errors.size\nputs errors.first_value\n\nerrors.each do |key, messages|\n  puts key   # =\u003e \"username\"\n  puts messages # =\u003e [\"The username is required.\", \"etc...\"]\nend\n```\n\n3 methods [#check](https://nicolab.github.io/crystal-validator/Check/Validation.html#instance-method-summary):\n\n```crystal\n# check(key : Symbol | String, valid : Bool)\n# Using default error message\nv.check(\n  \"username\",\n  is(:min?, user[\"username\"], 2)\n)\n\n# check(key : Symbol | String, message : String, valid : Bool)\n# Using custom error message\nv.check(\n  \"username\",\n  \"The username must contain at least 2 characters.\",\n  is(:min?, user[\"username\"], 2)\n)\n\n# check(key : Symbol | String, valid : Bool, message : String)\n# Using custom error message\nv.check(\n  \"username\",\n  is(:min?, user[\"username\"], 2),\n  \"The username must contain at least 2 characters.\"\n)\n```\n\n`Check` is a simple and lightweight wrapper.\nThe `Check::Validation` is agnostic of the checked data,\nof the context (model, controller, CSV file, HTTP data, socket data, JSON, etc).\n\n\u003e Use case example:\n  Before saving to the database or process user data for a particular task,\n  the custom error messages can be used for the end user response.\n\nBut a `Validation` instance can be used just to store validation errors:\n\n```crystal\nv = Check.new_validation\nv.add_error(\"foo\", \"foo error!\")\npp v.errors # =\u003e {\"foo\" =\u003e [\"foo error!\"]}\n```\n\n\u003e See also `Check.rules` and `Check.checkable`.\n\nLet your imagination run wild to add your logic around it.\n\n### Custom validator\n\nJust add your own method to register a custom *validator* or to overload an existing *validator*.\n\n```crystal\nmodule Validator\n  # My custom validator\n  def self.my_validator?(value, arg : String, another_arg : Int32) : Bool\n    # write here the logic of your validator...\n    return true\n  end\nend\n\n# Call it\nputs Valid.my_validator?(\"value to validate\", \"hello\", 42) # =\u003e true\n\n# or with the `is` flavor\nputs is :my_validator?, \"value to validate\", \"hello\", 42 # =\u003e true\n```\n\nUsing the custom validator with the validation rules:\n\n```crystal\nrequire \"validator/check\"\n\nclass Article\n  # Mixin\n  Check.checkable\n\n  property title : String\n  property content : String\n\n  Check.rules(\n    content: {\n      # Now the custom validator is available\n      check: {\n        my_validator: {\"My validator error message\"},\n        between: {\"The article content must be between 10 and 20 000 characters\", 10, 20_000},\n        # ...\n      },\n    },\n  )\nend\n\n# Triggered with all data\nv, article = Article.check(input_data)\n\n# Triggered with one value\nv, content = Article.check_content(input_data[\"content\"]?)\n```\n\n## Conventions\n\n* The word \"validator\" is the method to make a \"validation\" (value validation).\n* A *validator* returns `true` if the value (or/and the condition) is valid, `false` if not.\n* The first argument(s) is (are) the value(s) to be validated.\n* Always add the `Bool` return type to a *validator*.\n* Always add the suffix `?` to the method name of a *validator*.\n* If possible, indicates the type of the *validator* arguments.\n* Spec: Battle tested.\n* [KISS](https://en.wikipedia.org/wiki/KISS_principle) and [Unix Philosophy](https://en.wikipedia.org/wiki/Unix_philosophy).\n\n## Development\n\n```sh\ncrystal spec\ncrystal tool format\n./bin/ameba\n```\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/nicolab/crystal-validator/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n\n## LICENSE\n\n[MIT](https://github.com/Nicolab/crystal-validator/blob/master/LICENSE) (c) 2020, Nicolas Talle.\n\n## Author\n\n| [![Nicolas Tallefourtane - Nicolab.net](https://www.gravatar.com/avatar/d7dd0f4769f3aa48a3ecb308f0b457fc?s=64)](https://github.com/sponsors/Nicolab) |\n|---|\n| [Nicolas Talle](https://github.com/sponsors/Nicolab) |\n| [![Make a donation via Paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick\u0026hosted_button_id=PGRH4ZXP36GUC) |\n\n\u003e Thanks to [ilourt](https://github.com/ilourt) for his great work on `checkable` mixins (clean_*, check_*, ...).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicolab%2Fcrystal-validator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnicolab%2Fcrystal-validator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnicolab%2Fcrystal-validator/lists"}