Errors
Zod has Errors and Issues, issues are the structure thrown by Zod while Issues are the issues generated by the validation tests. Since Zog does not throw issues we use the term Issues to keep consistency with Zod.
Issues in Zog
In zog issues represent something that went wrong during any step of the parsing execution structure. Based on the schema you are using the returned issues will be in a different format:
ZogIssueList
For Primitive Types
Zog returns a list of ZogIssue
instances.
// will return []z.ZogIssue{z.ZogIssue{Message: "min length is 5"}, z.ZogIssue{Message: "invalid email"}}
errList := z.String().Min(5).Email().Parse("foo", &dest)
ZogIssueMap
For Complex Types
Zog returns a map of ZogIssue
instances. Which uses the field path as the key & the list of issues as the value.
// will return map[string][]z.ZogIssue{"name": []z.ZogIssue{z.ZogIssue{Message: "min length is 5"}}}
errMap := z.Struct(z.Schema{"name": z.String().Min(5)}).Parse(data, &dest)
// will return map[string][]z.ZogIssue{"$root": []z.ZogIssue{{Message: "slice length is not 2"}, "[0]": []z.ZogIssue{{Message: "min length is 10"}}}}
errsMap2 := z.Slice(z.String().Min(10)).Len(2).Parse([]string{"only_one"}, &dest)
// nested schemas will use the . or the [] notation to access the issues
errsMap3 := z.Struct(z.Schema{"name": z.String().Min(5), "address": z.Struct(z.Schema{"streets": z.Slice(z.String().Min(10))})}).Parse(data, &dest)
errsMap3["address.streets[0]"] // will return []z.ZogIssue{{Message: "min length is 10"}}
$root
& $first
are reserved keys for complex type validation, they are used for root level issues and for the first issue found in a schema, for example:
errsMap := z.Slice(z.String()).Min(2).Parse([]string{"only_one"}, &dest)
errsMap["$root"] // will return []z.ZogIssue{{Message: "slice length should at least be 2"}}
errsMap["$first"] // will return the same in this case []z.ZogIssue{{Message: "slice length should at least be 2"}}
The ZogIssue interface
The ZogIssue
is actually an interface which also implements the issue interface so it can be used with the issues
package. The issue interface is as follows:
// Error interface returned from all schemas
type ZogIssue interface {
// returns the issue code for the issue. This is a unique identifier for the issue. Generally also the ID for the Test that caused the issue.
Code() zconst.ZogIssueCode
// returns the data value that caused the issue.
// if using Schema.Parse(data, dest) then this will be the value of data.
Value() any
// Returns destination type. i.e The zconst.ZogType of the value that was validated.
// if Using Schema.Parse(data, dest) then this will be the type of dest.
Dtype() string
// returns the params map for the issue. Taken from the Test that caused the issue. This may be nil if Test has no params.
Params() map[string]any
// returns the human readable, user-friendly message for the issue. This is safe to expose to the user.
Message() string
// sets the human readable, user-friendly message for the issue. This is safe to expose to the user.
SetMessage(string)
// returns the string representation of the ZogIssue (same as String())
Error() string
// returns the wrapped issue or nil if none
Unwrap() issue
// returns the string representation of the ZogIssue (same as Error())
String() string
}
// When printed it looks like this:
// ZogIssue{Code: coercion_issue, Params: map[], Type: number, Value: not_empty, Message: number is invalid, Error: failed to coerce string int: strconv.Atoi: parsing "not_empty": invalid syntax}
Error Codes
Error codes are unique identifiers for each type of issue that can occur in Zog. They are used to generate issue messages and to identify the issue in the issue formatter. A full updated list of issue codes can be found in the zconst package. But here are some common ones:
type ZogIssueCode = string
const (
IssueCodeCustom ZogIssueCode = "custom" // all
IssueCodeRequired ZogIssueCode = "required" // all
IssueCodeCoerce ZogIssueCode = "coerce" // all
IssueCodeFallback ZogIssueCode = "fallback" // all. Applied when other errror code is not implemented. Required to be implemented for every zog type!
IssueCodeEQ ZogIssueCode = "eq" // number, time, string
IssueCodeOneOf ZogIssueCode = "one_of_options" // string or number
IssueCodeMin ZogIssueCode = "min" // string, slice
IssueCodeMax ZogIssueCode = "max" // string, slice
IssueCodeLen ZogIssueCode = "len" // string, slice
IssueCodeContains ZogIssueCode = "contained" // string, slice
// number only
IssueCodeLTE ZogIssueCode = "lte" // number
IssueCodeLT ZogIssueCode = "lt" // number
IssueCodeGTE ZogIssueCode = "gte" // number
IssueCodeGT ZogIssueCode = "gt" // number
// string only
IssueCodeEmail ZogIssueCode = "email"
IssueCodeUUID ZogIssueCode = "uuid"
IssueCodeMatch ZogIssueCode = "match"
IssueCodeURL ZogIssueCode = "url"
IssueCodeHasPrefix ZogIssueCode = "prefix"
IssueCodeHasSuffix ZogIssueCode = "suffix"
IssueCodeContainsUpper ZogIssueCode = "contains_upper"
IssueCodeContainsLower ZogIssueCode = "contains_lower"
IssueCodeContainsDigit ZogIssueCode = "contains_digit"
IssueCodeContainsSpecial ZogIssueCode = "contains_special"
// time only
IssueCodeAfter ZogIssueCode = "after"
IssueCodeBefore ZogIssueCode = "before"
// bool only
IssueCodeTrue ZogIssueCode = "true"
IssueCodeFalse ZogIssueCode = "false"
// ZHTTP ERRORS
IssueCodeZHTTPInvalidJSON ZogIssueCode = "invalid_json" // invalid json body
IssueCodeZHTTPInvalidForm ZogIssueCode = "invalid_form" // invalid form data
IssueCodeZHTTPInvalidQuery ZogIssueCode = "invalid_query" // invalid query params
)
Custom Error Messages
Zog has multiple ways of customizing issue messages as well as support for i18n. Here is a list of the ways you can customize issue messages:
1. Using the z.Message() function
This is a function available for all tests, it allows you to set a custom message for the test.
err := z.String().Min(5, z.Message("string must be at least 5 characters long")).Parse("bad", &dest)
// err = []ZogIssue{{Message: "string must be at least 5 characters long"}}
2. Using the z.MessageFunc() function
This is a function available for all tests, it allows you to set a custom message for the test.
This function takes in an IssueFmtFunc
which is the function used to format issue messages in Zog. It has the following signature:
type IssueFmtFunc = func(e ZogIssue, p Ctx)
err := z.String().Min(5, z.MessageFunc(func(e z.ZogIssue, p z.Ctx) {
e.SetMessage("string must be at least 5 characters long")
})).Parse("bad", &dest)
// err = []ZogIssue{{Message: "string must be at least 5 characters long"}}
3. Using the WithIssueFormatter() ExecOption
This allows you to set a custom ZogIssue
formatter for the entire parsing operation. Beware you must handle all ZogIssue
codes & types or you may get unexpected messages.
err := z.String().Min(5).Email().Parse("zog", &dest, z.WithIssueFormatter(func(e z.ZogIssue, p z.Ctx) {
e.SetMessage("override message")
}))
// err = []ZogIssue{{Code: min_length_issue, Message: "override message"}, {Code: email_issue, Message: "override message"}}
See how our issue messages were overridden? Be careful when using this!
4. Iterate over the returned issues and create custom messages
errs := userSchema.Parse(data, &user)
msgs := FormatZogIssues(errs)
func FormatZogIssues(errs z.ZogIssueMap) map[string][]string {
// iterate over issues and create custom messages based on the issue code, the params and destination type
}
5. Configure issue messages globally
Zog provides a conf
package where you can override the issue messages for specific issue codes. You will have to do a little digging to be able to do this. But here is an example:
import (
conf "github.com/Oudwins/zog/zconf"
zconst "github.com/Oudwins/zog/zconst"
)
// override specific issue messages
// For this I recommend you import `zog/zconst` which contains zog constants but you can just use strings if you prefer
conf.DefaultIssueMessageMap[zconst.TypeString]["my_custom_issue_code"] = "my custom issue message"
conf.DefaultIssueMessageMap[zconst.TypeString][zconst.IssueCodeRequired] = "Now all required issues will get this message"
But you can also outright override the issue formatter and ignore the issues map completely:
// override the issue formatter function - CAREFUL with this you can set every issue message to the same thing!
conf.IssueFormatter = func(e p.ZogIssue, p z.Ctx) {
// do something with the issue
...
// fallback to the default issue formatter
conf.DefaultIssueFormatter(e, p) // this uses the DefaultErrMsgMap to format the issue messages
}
6. Use the i18n package
Really this only makes sense if you are doing i18n. Please please check out the i18n section for more information.
Sanitizing ZogIssues
If you want to return issues to the user without the possibility of exposing internal confidential information, you can use the Zog sanitizer functions z.Issues.SanitizeMap(ZogIssueMap)
or z.Issues.SanitizeSlice(ZogIssueList)
. These functions will return a map or slice of strings of the issue messages (stripping out all the internal data from the issues like the error that caused the issue, the path, params, etc).
errs := userSchema.Parse(data, &user)
// errs = map[string][]ZogIssue{"name": []ZogIssue{{Message: "min length is 5"}, {Message: "max length is 10"}}, "email": []ZogIssue{{Message: "is not a valid email"}}}
sanitized := z.Issues.SanitizeMap(errs)
// sanitized = {"name": []string{"min length is 5", "max length is 10"}, "email": []string{"is not a valid email"}}