Formatting error responses
Zog emphasizes completeness and correctness in its error reporting. In many cases, it's helpful to convert the ZogIssueList to a more useful format. Zog provides some utilities for this.
Consider this simple slice schema.
schema := z.Slice(z.String().Required().HasPrefix("PREFIX_")).Min(3);
Attempting to parse/validate this invalid data results in an error containing three issues.
s := []string{"one", "two"}
errs := schema.Parse(&s)
errs;
[
{
code: 'prefix',
path: []string{"[0]"}
message: "string must start with 'PREFIX_'"
},
{
code: 'prefix',
path: []string{"[1]"}
message: "string must start with 'PREFIX_'"
},
{
code: "min",
path: nil,
message: "slice must contain at least 3 items"
}
]
z.Issues.Flatten()
This converts the errors list to a map[path][]messages. For the example above it would generate:
{
"$root": ["string must start with 'PREFIX_'"], // Use zconst.ISSUE_KEY_ROOT for a constant of this key!
"[0]": ["string must start with 'PREFIX_'"],
"[1]": ["slice must contain at least 3 items"]
}
How does flatten logic work? It follows a few simple rules:
- issues with a nil or empty path will be assigned to
$rootreserved key- Struct/map keys are mapped to their key names and joined by
.(For exampleuser.firstnamewhere the path is[]string{"user", "firstname"})- Slices are mapped to their index and can be appended to a previous struct/map key. For example
[]string{"[0]"},[]string{"[0]", "firstname"}and[]string{"users", "[0]", "firstname"}are all valid paths
z.Issues.Treeify()
This converts the errors list into a nested tree structure that mirrors the original data structure. This format is particularly useful when you need to display errors in a hierarchical way that matches your form or data model.
For the example above, Treeify would generate:
{
"errors": ["slice must contain at least 3 items"],
"properties": {
"items": [
{
"errors": ["string must start with 'PREFIX_'"]
},
{
"errors": ["string must start with 'PREFIX_'"]
}
]
}
}
Here's a more complex example with nested structures:
schema := z.Struct{
"user": z.Struct{
"name": z.String().Min(3),
"email": z.String().Email(),
},
"users": z.Slice(z.Struct{
"name": z.String().Required(),
}),
}
data := map[string]any{
"user": map[string]any{
"name": "ab", // too short
"email": "invalid", // not an email
},
"users": []any{
map[string]any{"name": ""}, // required
map[string]any{"name": "ok"},
},
}
errs := schema.Parse(data, &dest)
tree := z.Issues.Treeify(errs)
The resulting tree structure:
{
"errors": [],
"properties": {
"user": {
"errors": [],
"name": {
"errors": ["string must be at least 3 characters"]
},
"email": {
"errors": ["string must be a valid email"]
}
},
"users": {
"errors": [],
"items": [
{
"errors": [],
"name": {
"errors": ["string is required"]
}
},
null
]
}
}
}
How does treeify logic work? The tree structure follows these rules:
- Root-level errors (nil or empty path) are placed in the top-level
errorsarray- Property errors create nested objects under
properties, with each path segment becoming a nested level- Array indices create an
itemsarray within the parent property, with each index becoming an element in the array- Numeric string segments (like
"0") are treated as array indices and createitemsarrays- Each node in the tree has an
errorsarray, even if empty, to maintain consistent structure
z.Issues.Prettify()
This formats the errors list into a human-readable string representation. Each issue is displayed with a "✖" prefix, and issues with paths include the path information on a separate line with a "→ at" prefix. This format is ideal for displaying errors directly to users in console output or error messages.
For the example above, Prettify would generate:
✖ string must start with 'PREFIX_'
→ at [0]
✖ string must start with 'PREFIX_'
→ at [1]
✖ slice must contain at least 3 items
Here's a more complex example:
schema := z.Struct{
"user": z.Struct{
"name": z.String().Min(3),
"email": z.String().Email(),
},
"users": z.Slice(z.Struct{
"name": z.String().Required(),
}),
}
data := map[string]any{
"user": map[string]any{
"name": "ab", // too short
"email": "invalid", // not an email
},
"users": []any{
map[string]any{"name": ""}, // required
},
}
errs := schema.Parse(data, &dest)
pretty := z.Issues.Prettify(errs)
The resulting formatted string:
✖ string must be at least 3 characters
→ at user.name
✖ string must be a valid email
→ at user.email
✖ string is required
→ at users[0].name
How does prettify logic work? The formatting follows these rules:
- Empty issue lists return an empty string
- Each issue message is prefixed with "✖ " (checkmark symbol)
- Issues with paths (that flatten to a non-empty string) include the path on a new line with " → at " prefix
- Multiple issues are separated by newlines (
\n)- Root-level errors (nil or empty path) are displayed without a path line
- Paths are flattened using the same logic as
Flatten(), so nested properties use dot notation (e.g.,user.name) and array indices use bracket notation (e.g.,users[0])