{"id":15661896,"url":"https://github.com/nikolaydubina/validate","last_synced_at":"2025-04-15T22:50:48.299Z","repository":{"id":42512351,"uuid":"476300454","full_name":"nikolaydubina/validate","owner":"nikolaydubina","description":"🥬 validate. simply.","archived":false,"fork":false,"pushed_at":"2024-08-22T09:17:16.000Z","size":43,"stargazers_count":20,"open_issues_count":6,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-11T21:13:46.987Z","etag":null,"topics":["generics","go","haiku","validation"],"latest_commit_sha":null,"homepage":"","language":"Go","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/nikolaydubina.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2022-03-31T12:41:38.000Z","updated_at":"2024-09-04T12:39:59.000Z","dependencies_parsed_at":"2024-10-23T07:22:07.832Z","dependency_job_id":null,"html_url":"https://github.com/nikolaydubina/validate","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Fvalidate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Fvalidate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Fvalidate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nikolaydubina%2Fvalidate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nikolaydubina","download_url":"https://codeload.github.com/nikolaydubina/validate/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249167434,"owners_count":21223505,"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":["generics","go","haiku","validation"],"created_at":"2024-10-03T13:29:26.840Z","updated_at":"2025-04-15T22:50:48.284Z","avatar_url":"https://github.com/nikolaydubina.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🥬 validate. simply.\n\n\u003e no reflection. no gencode. hierarchical and extendable. fast. ~100LOC. generics.\n\n[![codecov](https://codecov.io/gh/nikolaydubina/validate/branch/main/graph/badge.svg?token=76JC6fX7DP)](https://codecov.io/gh/nikolaydubina/validate)\n[![Go Reference](https://pkg.go.dev/badge/github.com/nikolaydubina/validate.svg)](https://pkg.go.dev/github.com/nikolaydubina/validate)\n[![Go Report Card](https://goreportcard.com/badge/github.com/nikolaydubina/validate)](https://goreportcard.com/report/github.com/nikolaydubina/validate)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/nikolaydubina/validate/badge)](https://securityscorecards.dev/viewer/?uri=github.com/nikolaydubina/validate)\n\nThis is convenient when you have custom validation and nested structures.  \n\n```go\n// Employee is example of struct with validatable fields and nested structure\ntype Employee struct {\n\tName          string\n\tAge           int\n\tColor         Color     // custom func Validate()\n\tEducation     Education // nested with Validate()\n\tSalary        float64\n\tExperience    time.Duration\n\tBirthday      time.Time\n\tVacationStart time.Time\n}\n\nfunc (s Employee) Validate() error {\n\treturn validate.All(\n\t\tvalidate.OneOf(\"name\", s.Name, \"Zeus\", \"Hera\"),\n\t\tvalidate.OneOf(\"age\", s.Age, 35, 55),\n\t\tvalidate.Min(\"age\", s.Age, 10), // same field validated again\n\t\ts.Color.Validate(),\n\t\ts.Education.Validate(),\n\t\tvalidate.Max(\"salary\", s.Salary, 123.456),\n\t\tvalidate.Max(\"duration\", s.Experience, time.Duration(1)*time.Hour),\n\t\tvalidate.After(\"birthday\", s.Birthday, time.Date(1984, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\tvalidate.Before(\"vacation_start\", s.VacationStart, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t)\n}\n\n// Education is another custom struct\ntype Education struct {\n\tDuration   int\n\tSchoolName string\n}\n\nfunc (e Education) Validate() error {\n\tif (e.Duration % 17) == 5 {\n\t\treturn errors.New(\"my special error\")\n\t}\n\treturn validate.All(\n\t\tvalidate.Min(\"\", e.Duration, 10),\n\t\tvalidate.OneOf(\"\", e.SchoolName, \"KAIST\", \"Stanford\"),\n\t)\n}\n\n// Color is custom enum\ntype Color string\n\nconst (\n\tRed   Color = \"red\"\n\tGreen Color = \"green\"\n\tBlue  Color = \"blue\"\n)\n\nfunc (s Color) Validate() error {\n\tswitch s {\n\tcase Red, Green, Blue:\n\t\treturn nil\n\tdefault:\n\t\treturn fmt.Errorf(\"wrong value(%s), expected(%v)\", s, []Color{\n\t\t\t\"red\",\n\t\t\t\"green\",\n\t\t\t\"blue\",\n\t\t})\n\t}\n}\n```\n\nExample error message:\n```\nvalidate: 8 errors: [name(Bob) not in [Zeus Hera]; age(101) not in [35 55]; color wrong value(orange), expected([red green blue]); validate: 1 errors: [(Berkeley) not in [KAIST Stanford]]; salary(256.99) higher than max (123.456); duration(10h0m0s) higher than max (1h0m0s); birthday(1984-01-01 00:00:00 +0000 UTC) is not after (1984-01-01 00:00:00 +0000 UTC); vacation_start(2025-01-01 00:00:00 +0000 UTC) is not before (2024-01-01 00:00:00 +0000 UTC)]\n```\n\n## Implementation Details\n\nPrinting error takes a lot of time. \nThus, it is good to delay constructor of error message as much as possible.\nAnd sometimes user code does not need to print error at all and only `nil` check is performed.\nThis is done by moving construction of error message in `Error` methods.\n\nIt is advisable to avoid memory allocations and creation of structures.\nSuch in case of success flow, we ideally will not have any memory allocations at all.\nThis is why we make validators as functions and call them in chain.\nWe do not delay nor wrap validation function calls.\nWe use function arguments as storage for validation parameters, they are simple params and likely to be on stack which is fast.\nFor example, for `OneOf` we are using variadic arguments.\nOther alternative is to use arrays since in Go they are on stack as well.\n\nWe also hope Go compiler\n- can detect that argument to function is constant and inline it in assembly or stack\n- does not use expensive memory for variadic parameters\n- can inline functions\n\nDefining custom validators with `switch` is expected to be even faster.\n\n## Benchmarks\n\n```\n$ go test -bench=. -benchtime=10s -benchmem ./...\ngoos: darwin\ngoarch: amd64\npkg: github.com/nikolaydubina/validate\ncpu: VirtualApple @ 2.50GHz\nBenchmarkEmployee_Error_Message-10                \t   3744121\t      3229 ns/op\t    2376 B/op\t      56 allocs/op\nBenchmarkEmployee_Error-10                        \t  12533948\t       958 ns/op\t     904 B/op\t      23 allocs/op\nBenchmarkEmployee_Success-10                      \t 100000000\t       115 ns/op\t      80 B/op\t       3 allocs/op\nBenchmarkEmployeeSimple_Error_Message-10          \t   9488436\t      1263 ns/op\t     840 B/op\t      25 allocs/op\nBenchmarkEmployeeSimple_Error-10                  \t  44261380\t       270 ns/op\t     344 B/op\t       9 allocs/op\nBenchmarkEmployeeSimple_Success-10                \t 243491635\t        49 ns/op\t      48 B/op\t       2 allocs/op\nBenchmarkEmployeeNoContainers_Error_Message-10    \t  28089966\t       427 ns/op\t     248 B/op\t       9 allocs/op\nBenchmarkEmployeeNoContainers_Error-10            \t 142881793\t        85 ns/op\t      88 B/op\t       3 allocs/op\nBenchmarkEmployeeNoContainers_Success-10        \t1000000000\t         4 ns/op\t       0 B/op\t       0 allocs/op\nPASS\nok  \tgithub.com/nikolaydubina/validate\t120.565s\n```\n\n## Appendix A: Comparison to other validators\n\n#### `github.com/go-playground/validator`\n\nIt uses struct tags and reflection.\nBinding custom validations require defining validation function with special name and using interface typecast then registering this to validator instance.\n\nIt has instance of validator that is reused.\n\nIts speed is mostly few hundred ns and up to 1µs.\nIts memory allocation can be 0 and reaches up to few dozen.\n\n## Appendix B: Wrapping validators into interface\n\nEarly version of this library was wrapping each validation operation into a `interface { Validate() error }`.\nIn this approach, we already had in validators everything needed to format error message, which is why we were reusing them as error containers.\nHowever, there were few drawbacks.\n\nCode looked more verbose:\n```go\nfunc (s Employee) Validate() error {\n\treturn validate.All(\n\t\tvalidate.OneOf[string]{Name: \"name\", Value: s.Name, Values: []string{\"Zeus\", \"Hera\"}},\n\t\tvalidate.OneOf[int]{Name: \"age\", Value: s.Age, Values: []int{35, 55}},\n\t\tvalidate.Min[int]{Name: \"age\", Value: s.Age, Min: 10}, // same field validated again\n\t\ts.Color,\n\t\ts.Education,\n\t\tvalidate.Max[float64]{Name: \"salary\", Value: s.Salary, Max: 123.456},\n\t\tvalidate.Max[time.Duration]{Name: \"duration\", Value: s.Experience, Max: time.Duration(1) * time.Hour},\n\t\tvalidate.After{Name: \"birthday\", Value: s.Birthday, Time: time.Date(1984, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t\tvalidate.Before{Name: \"vacation_start\", Value: s.VacationStart, Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)},\n\t)\n}\n```\n\nPerformance was slightly worse for error case, and much worse for success case:\n```\n$ go test -bench=. -benchtime=10s -benchmem ./...\ngoos: darwin\ngoarch: amd64\npkg: github.com/nikolaydubina/validate\ncpu: VirtualApple @ 2.50GHz\nBenchmarkEmployee_Error_Message-10                \t 3579223\t      3379 ns/op\t    2761 B/op\t      62 allocs/op\nBenchmarkEmployee_Error-10                        \t 9361948\t      1277 ns/op\t    1344 B/op\t      34 allocs/op\nBenchmarkEmployee_Success-10                      \t25418672\t       474 ns/op\t     552 B/op\t      14 allocs/op\nBenchmarkEmployeeSimple_Error_Message-10          \t 8757170\t      1364 ns/op\t     992 B/op\t      28 allocs/op\nBenchmarkEmployeeSimple_Error-10                  \t30418941\t       394 ns/op\t     504 B/op\t      13 allocs/op\nBenchmarkEmployeeSimple_Success-10                \t65194581\t       184 ns/op\t     224 B/op\t       6 allocs/op\nBenchmarkEmployeeNoContainers_Error_Message-10    \t24971338\t       483 ns/op\t     280 B/op\t      10 allocs/op\nBenchmarkEmployeeNoContainers_Error-10            \t72736639\t       165 ns/op\t     136 B/op\t       5 allocs/op\nBenchmarkEmployeeNoContainers_Success-10          \t143333276\t        83 ns/op\t      64 B/op\t       2 allocs/op\nPASS\nok  \tgithub.com/nikolaydubina/validate\t124.950s\n```\n\n## Appendix C: Binding validator functions in map to field names\n\nIt might appear that it is more efficient not to pass `name` of field in validator.\nSuch it is tempting to run slice or map of functions.\nHowever, performance deteriorates with this approach.\nLikely this is due to compiler using stack or not efficiently inlining.\n\nCode sample:\n```go\nfunc All(vs map[string]error) error {\n\terrs := make(map[string]error, len(vs))\n\tfor k, err := range vs {\n\t\tif err != nil {\n\t\t\terrs[k] = err\n\t\t}\n\t}\n\tif len(errs) \u003e 0 {\n\t\treturn errMultiple(errs)\n\t}\n\treturn nil\n}\n\n...\n\nfunc (s Employee) Validate() error {\n\treturn validate.All(map[string]error{\n\t\t\"name\":           validate.OneOf(s.Name, \"Zeus\", \"Hera\"),\n\t\t\"age\":            validate.OneOf(s.Age, 35, 55),\n\t\t\"age_2\":          validate.Min(s.Age, 10), // same field validated again\n\t\t\"color\":          s.Color.Validate(),\n\t\t\"education\":      s.Education.Validate(),\n\t\t\"salary\":         validate.Max(s.Salary, 123.456),\n\t\t\"duration\":       validate.Max(s.Experience, time.Duration(1)*time.Hour),\n\t\t\"birthday\":       validate.After(s.Birthday, time.Date(1984, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t\t\"vacation_start\": validate.Before(s.VacationStart, time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)),\n\t})\n}\n```\n\nPerformance is worse across the board:\n```\ngoos: darwin\ngoarch: amd64\npkg: github.com/nikolaydubina/validate\ncpu: VirtualApple @ 2.50GHz\nBenchmarkEmployee_Error_Message-10                \t 2698065\t      4359 ns/op\t    3866 B/op\t      57 allocs/op\nBenchmarkEmployee_Error-10                        \t 6993564\t      1714 ns/op\t    2058 B/op\t      21 allocs/op\nBenchmarkEmployee_Success-10                      \t14948445\t       810 ns/op\t    1329 B/op\t       7 allocs/op\nBenchmarkEmployeeSimple_Error_Message-10          \t 8243392\t      1460 ns/op\t    1112 B/op\t      26 allocs/op\nBenchmarkEmployeeSimple_Error-10                  \t33798837\t       356 ns/op\t     496 B/op\t       7 allocs/op\nBenchmarkEmployeeSimple_Success-10                \t66953932\t       182 ns/op\t      96 B/op\t       3 allocs/op\nBenchmarkEmployeeNoContainers_Error_Message-10    \t19754359\t       600 ns/op\t     576 B/op\t      10 allocs/op\nBenchmarkEmployeeNoContainers_Error-10            \t61285670\t       194 ns/op\t     368 B/op\t       3 allocs/op\nBenchmarkEmployeeNoContainers_Success-10          \t123293473\t        98 ns/op\t      48 B/op\t       1 allocs/op\nPASS\nok  \tgithub.com/nikolaydubina/validate\t128.263s\n```\n\n## Reference\n\n- As of `2022-04-01`, Go does not support generic arrays.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikolaydubina%2Fvalidate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnikolaydubina%2Fvalidate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnikolaydubina%2Fvalidate/lists"}