{"id":27380126,"url":"https://github.com/metafates/schema","last_synced_at":"2025-04-13T14:19:08.879Z","repository":{"id":286287821,"uuid":"960960892","full_name":"metafates/schema","owner":"metafates","description":"📐 Go type-safe validation library. No field tags or map[string]any schemas, pure types!","archived":false,"fork":false,"pushed_at":"2025-04-12T18:32:51.000Z","size":130,"stargazers_count":62,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T18:41:29.565Z","etag":null,"topics":["codegen","generics","go","library","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/metafates.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,"publiccode":null,"codemeta":null}},"created_at":"2025-04-05T12:58:23.000Z","updated_at":"2025-04-12T18:32:54.000Z","dependencies_parsed_at":"2025-04-05T14:23:14.552Z","dependency_job_id":"412357aa-ea03-4934-9af0-71a8908adce8","html_url":"https://github.com/metafates/schema","commit_stats":null,"previous_names":["metafates/schema"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metafates%2Fschema","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metafates%2Fschema/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metafates%2Fschema/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/metafates%2Fschema/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/metafates","download_url":"https://codeload.github.com/metafates/schema/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248724627,"owners_count":21151561,"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":["codegen","generics","go","library","validation"],"created_at":"2025-04-13T14:19:08.305Z","updated_at":"2025-04-13T14:19:08.872Z","avatar_url":"https://github.com/metafates.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 📐 Schema\n\n\u003e Work in progress!\n\nGo schema declaration and validation with static types.\nNo field tags or code duplication.\n\nSchema is designed to be as developer-friendly as possible.\nThe goal is to eliminate duplicative type declarations.\nYou declare a schema once and it will be used as both schema and type itself.\nIt's easy to compose simpler types into complex data structures.\n\nNo stable version yet, but you can use it like that.\n\n```bash\ngo get github.com/metafates/schema@main\n```\n\n**Work in progress, API may (and will) change significantly without further notice! This is just an experiment for now**\n\n## Features\n\n- Type-safe\n- Zero setup required\n- Zero overhead (can be achieved with optional codegen)\n- No DSL or code duplication\n- Cross-field validation support\n- Helpful errors\n\n## Example\n\nSee [examples](./_examples) for more examples\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\tschemajson \"github.com/metafates/schema/encoding/json\"\n\t\"github.com/metafates/schema/optional\"\n\t\"github.com/metafates/schema/required\"\n\t\"github.com/metafates/schema/validate\"\n)\n\n// Let's assume we have a request which accepts an user\ntype User struct {\n\t// User name is required and must not be empty\n\tName required.NonEmpty[string] `json:\"name\"`\n\n\t// Birth date is optional, which means it could be null.\n\t// However, if passed, it must be an any valid [time.Time]\n\tBirth optional.Any[time.Time] `json:\"birth\"`\n\n\t// Same for email. It is optional, therefore it could be null.\n\t// But, if passed, not only it must be a valid string, but also a valid RFC 5322 email string\n\tEmail optional.Email[string] `json:\"email\"`\n\n\t// Bio is just a regular string. It may be empty, may be not.\n\t// No further logic is attached to it.\n\tBio string `json:\"bio\"`\n}\n\n// We could also have an address\ntype Address struct {\n\t// Latitude is required and must be a valid latitude (range [-90; 90])\n\tLatitude required.Latitude[float64] `json:\"latitude\"`\n\n\t// Longitude is also required and must be a valid longitude (range [-180; 180])\n\tLongitude required.Longitude[float64] `json:\"longitude\"`\n}\n\n// But wait, what about custom types?\n// We might want (for some reason) a field which accepts only short strings (\u003c10 bytes).\n// Let's see how we might implement it.\n\n// let's define a custom validator for short strings.\n// it should not contain any fields in itself, they won't be initialized or used in any way.\ntype ShortStr struct{}\n\n// this function implements a special validator interface\nfunc (ShortStr) Validate(v string) error {\n\tif len(v) \u003e= 10 {\n\t\treturn errors.New(\"string is too long\")\n\t}\n\n\treturn nil\n}\n\n// that's it, basically. now we can use this validator in our request.\n\n// but we can go extreme! we can combine multiple validators using types\ntype ASCIIShortStr struct {\n\t// both ASCII and ShortStr must be satisfied.\n\t// you can also use [validate.Or] to ensure that at least one condition is satisfied.\n\tvalidate.And[\n\t\tstring,\n\t\tvalidate.ASCII[string],\n\t\tShortStr,\n\t]\n}\n\n// Now, our final request may look something like that\ntype Request struct {\n\t// User is required by default.\n\t// Because, if not passed, it will be empty.\n\t// Therefore required fields in user will also be empty which will result a missing fields error.\n\tUser User `json:\"user\"`\n\n\t// Address is, however, optional. It could be null.\n\t// But, if passed, it must be a valid address with respect to its fields validation (required lat/lon)\n\tAddress optional.Any[Address] `json:\"address\"`\n\n\t// this is how we can use our validator in custom type.\n\t// we could make an alias for that custom required type, if needed.\n\tMyShortString required.Custom[string, ShortStr] `json:\"myShortString\"`\n\n\t// same as for [Request.MyShortString] but using an optional instead.\n\tASCIIShortString optional.Custom[string, ASCIIShortStr] `json:\"asciiShortString\"`\n\n\tPermitBio bool `json:\"permitBio\"`\n}\n\n// - \"How do I do cross-field validation?\"\n// - Implement [validate.Validateable] interface for your struct\n//\n// This method will be called AFTER required and optional fields are validated.\n// It is optional - you may skip defining it if you don't need to.\nfunc (r *Request) Validate() error {\n\tif !r.PermitBio {\n\t\tif r.User.Bio != \"\" {\n\t\t\treturn errors.New(\"bio is not permitted\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc main() {\n\t// here we create a VALID json for our request. we will try to pass an invalid one later.\n\tdata := []byte(`\n{\n\t\"user\": {\n\t\t\"name\": \"john\",\n\t\t\"email\": \"john@example.com (comment)\",\n\t\t\"bio\": \"lorem ipsum\"\n\t},\n\t\"address\": {\n\t\t\"latitude\": 81.111,\n\t\t\"longitude\": 100.101\n\t},\n\t\"myShortString\": \"foo\",\n\t\"permitBio\": true\n}`)\n\n\tvar request Request\n\n\t{\n\t\t// let's unmarshal it. we can use anything we want, not only json\n\t\tif err := json.Unmarshal(data, \u0026request); err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\n\t\t// but validation won't happen just yet. we need to invoke it manually\n\t\t// (passing pointer to validate is required to maintain certain guarantees later)\n\t\tif err := validate.Validate(\u0026request); err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t\t// that's it, our struct was validated successfully!\n\t\t// no errors yet, but we will get there\n\t\t//\n\t\t// remember our custom cross-field validation?\n\t\t// it was called as part of this function\n\t}\n\n\t{\n\t\t// we could also use a helper function that does *exactly that* for us.\n\t\tif err := schemajson.Unmarshal(data, \u0026request); err != nil {\n\t\t\tlog.Fatalln(err)\n\t\t}\n\t}\n\n\t// now that we have successfully unmarshalled our json, we can use request fields.\n\t// to access values of our schema-guarded fields we can use Get() method\n\t//\n\t// NOTE: calling this method BEFORE we have\n\t// validated our request will panic intentionally.\n\tfmt.Println(request.User.Name.Get()) // output: john\n\n\t// optional values return a tuple: a value and a boolean stating its presence\n\temail, ok := request.User.Email.Get()\n\tfmt.Println(email, ok) // output: john@example.com (comment) true\n\n\t// birth is missing so \"ok\" will be false\n\tbirth, ok := request.User.Birth.Get()\n\tfmt.Println(birth, ok) // output: 0001-01-01 00:00:00 +0000 UTC false\n\n\t// let's try to pass an INVALID jsons.\n\tinvalidEmail := []byte(`\n{\n\t\"user\": {\n\t\t\"name\": \"john\",\n\t\t\"email\": \"john@@@example.com\",\n\t\t\"bio\": \"lorem ipsum\"\n\t},\n\t\"address\": {\n\t\t\"latitude\": 81.111,\n\t\t\"longitude\": 100.101\n\t},\n\t\"myShortString\": \"foo\",\n\t\"permitBio\": true\n}`)\n\n\tinvalidShortStr := []byte(`\n{\n\t\"user\": {\n\t\t\"name\": \"john\",\n\t\t\"email\": \"john@example.com\",\n\t\t\"bio\": \"lorem ipsum\"\n\t},\n\t\"address\": {\n\t\t\"latitude\": 81.111,\n\t\t\"longitude\": 100.101\n\t},\n\t\"myShortString\": \"super long string that shall not pass!!!!!!!!\",\n\t\"permitBio\": true\n}`)\n\n\tmissingUserName := []byte(`\n{\n\t\"user\": {\n\t\t\"email\": \"john@example.com\",\n\t\t\"bio\": \"lorem ipsum\"\n\t},\n\t\"address\": {\n\t\t\"latitude\": 81.111,\n\t\t\"longitude\": 100.101\n\t},\n\t\"myShortString\": \"foo\",\n\t\"permitBio\": true\n}`)\n\n\tbioNotPermitted := []byte(`\n{\n\t\"user\": {\n\t\t\"name\": \"john\",\n\t\t\"email\": \"john@example.com\",\n\t\t\"bio\": \"lorem ipsum\"\n\t},\n\t\"address\": {\n\t\t\"latitude\": 81.111,\n\t\t\"longitude\": 100.101\n\t},\n\t\"myShortString\": \"foo\",\n\t\"permitBio\": false\n}`)\n\n\tfmt.Println(schemajson.Unmarshal(invalidEmail, new(Request)))\n\t// validate: User.Email: mail: missing '@' or angle-addr\n\n\tfmt.Println(schemajson.Unmarshal(invalidShortStr, new(Request)))\n\t// validate: MyShortString: string is too long\n\n\tfmt.Println(schemajson.Unmarshal(missingUserName, new(Request)))\n\t// validate: User.Name: missing value\n\n\tfmt.Println(schemajson.Unmarshal(bioNotPermitted, new(Request)))\n\t// validate: bio is not permitted\n\n\t// You can check if it was validation error or any other json error.\n\terr := schemajson.Unmarshal(missingUserName, new(Request))\n\n\tvar validationErr validate.ValidationError\n\tif errors.As(err, \u0026validationErr) {\n\t\tfmt.Println(\"error while validating\", validationErr.Path())\n\t\t// error while validating User.Name\n\n\t\tfmt.Println(errors.Is(err, required.ErrMissingValue))\n\t\t// true\n\t}\n}\n```\n\n## Performance\n\n**TL;DR:** you can use codegen for max performance (0-1% overhead) or fallback to reflection (35% overhead).\n\nThis library does not affect unmarshalling performance itself.\nYou can expect it to be just as fast as a regular unmarshalling.\n\n**However!**\n\nValidation, by default, requires reflection to traverse over all struct fields. Again, reflection is only used to traverse fields, validators themself do not use reflection at all.\n\nSuch reflection traversal introduce ~35% performance overhead.\n\nAs an alternative, you can use [schemagen](./cmd/schemagen) [WIP] to generate field traversal logic.\nAs a result, overhead will reduced to 0-1% even for large structures. No need to change anything else.\n\nUsing reflection is easier because it does not require any codegen setup, but it does introduce minor performance decrease.\n\nUnless performance is top-priority and validation is indeed a bottleneck (usually it's not), I'd recommend sticking with the reflection - it makes your codebase simpler to maintain.\n\n**Benchmark:**\n\n```\ngoos: darwin\ngoarch: arm64\npkg: github.com/metafates/schema/bench\ncpu: Apple M3 Pro\nBenchmarkUnmarshalJSON/reflection/with_validation-12      66221 ns/op\nBenchmarkUnmarshalJSON/reflection/without_validation-12   45593 ns/op\nBenchmarkUnmarshalJSON/codegen/with_validation-12         45936 ns/op\nBenchmarkUnmarshalJSON/codegen/without_validation-12      45649 ns/op\n```\n\n## Validators\n\nNot listed here yet, but can see a full list\nof available validators in [validate/validate.go](./validate/validate.go)\n\n## TODO\n\n- [ ] Support for manual construction (similar to `.parse(...)` in zod) (using codegen)\n- [ ] Stabilize API\n- [ ] Better documentation\n- [x] More tests\n- [x] Improve performance. It should not be a bottleneck for most usecases, especially for basic CRUD apps. Still, there is a room for improvement!\n- [ ] Think about validating gRPC generated structs somehow (codegen?)\n- [ ] Add benchmarks for validators itself. E.g. email validator\n- [ ] More validation types as seen in https://github.com/go-playground/validator\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetafates%2Fschema","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmetafates%2Fschema","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmetafates%2Fschema/lists"}