Adding a New Constraint to the Validation Library
Follow this workflow when adding a new constraint. Mandatory steps: constraint in it, message constant, translations (english + russian), tests, examples, godoc. Optional: validate and is when useful for standalone validation (e.g. string codes, identifiers).
1. Message and Error (mandatory)
1.1 Add message constant
In message/messages.go add a new constant (English default text):
go1const ( 2 // ... existing 3 InvalidMyFormat = "This value is not a valid my format." 4)
- Use existing style:
InvalidXxx,NotXxx,TooXxx, etc. - Text is the default (English) template; placeholders like
{{ value }},{{ limit }}are allowed.
1.2 Add validation error
In errors.go (root package) add:
go1var ( 2 // ... existing 3 ErrInvalidMyFormat = NewError("invalid my format", message.InvalidMyFormat) 4)
- First argument: stable code (backward compatibility).
- Second argument: message constant from
messagepackage (used for translation key and default text).
2. Translations (mandatory)
Add the same key (the message constant) in both translation files.
2.1 English
In message/translations/english/messages.go:
go1var Messages = map[language.Tag]map[string]catalog.Message{ 2 language.English: { 3 // ... existing 4 message.InvalidMyFormat: catalog.String(message.InvalidMyFormat), 5 }, 6}
- For simple messages use
catalog.String(message.Const)orcatalog.String("Your English text."). - For plurals (e.g. "N elements") use
plural.Selectf(1, "", plural.One, "...", plural.Other, "..."). See reference.md for plural and Russian forms.
2.2 Russian
In message/translations/russian/messages.go:
go1var Messages = map[language.Tag]map[string]catalog.Message{ 2 language.Russian: { 3 // ... existing 4 message.InvalidMyFormat: catalog.String("Значение не является допустимым форматом."), 5 }, 6}
- Key is always the message constant from
messagepackage. - Value: Russian text; same placeholders as in the message constant (e.g.
{{ value }}). - For plurals use
plural.Selectf(1, "", plural.One, "...", plural.Few, "...", plural.Other, "...")— Russian has Few. See reference.md.
3. Constraint in package it (mandatory)
Choose the right file: it/string.go, it/identifiers.go, it/web.go, it/comparison.go, it/basic.go, it/iterable.go, it/date_time.go, it/choice.go, it/barcodes.go.
3.1 Simple string constraint (func(string) bool)
If the check is a pure func(string) bool, use OfStringBy and the is helper:
go1// IsMyFormat validates whether the value is in my format. 2// See [link] for specification. 3func IsMyFormat() validation.StringFuncConstraint { 4 return validation.OfStringBy(is.MyFormat). 5 WithError(validation.ErrInvalidMyFormat). 6 WithMessage(validation.ErrInvalidMyFormat.Message()) 7}
3.2 Custom struct constraint
When you need options (e.g. versions, formats), define a struct and implement ValidateString:
go1// MyConstraint validates whether the string value satisfies my format. 2// Use [MyConstraint.Option] to configure. 3type MyConstraint struct { 4 isIgnored bool 5 groups []string 6 options []func(o *validate.MyOptions) 7 err error 8 messageTemplate string 9 messageParameters validation.TemplateParameterList 10} 11 12// IsMy creates the constraint. 13func IsMy() MyConstraint { 14 return MyConstraint{ 15 err: validation.ErrInvalidMyFormat, 16 messageTemplate: validation.ErrInvalidMyFormat.Message(), 17 } 18} 19 20// WithError overrides default error for produced violation. 21func (c MyConstraint) WithError(err error) MyConstraint { ... } 22 23// WithMessage sets the violation message template. 24func (c MyConstraint) WithMessage(template string, parameters ...validation.TemplateParameter) MyConstraint { ... } 25 26// When / WhenGroups for conditional validation. 27func (c MyConstraint) When(condition bool) MyConstraint { ... } 28func (c MyConstraint) WhenGroups(groups ...string) MyConstraint { ... } 29 30func (c MyConstraint) ValidateString(ctx context.Context, validator *validation.Validator, value *string) error { 31 if c.isIgnored || validator.IsIgnoredForGroups(c.groups...) || value == nil || *value == "" { 32 return nil 33 } 34 if is.My(*value, c.options...) { 35 return nil 36 } 37 return validator.BuildViolation(ctx, c.err, c.messageTemplate). 38 WithParameters( 39 c.messageParameters.Prepend( 40 validation.TemplateParameter{Key: "{{ value }}", Value: *value}, 41 )..., 42 ). 43 Create() 44}
Template rules:
- Empty/nil: Usually skip (return
nil); useit.IsNotBlank()(or similar) to reject empty. - Violation: Use
validator.BuildViolation(ctx, c.err, c.messageTemplate).WithParameters(...).Create(). Do not useCreateViolationfor translatable constraints — useBuildViolationso the message is translated. - Godoc: Document the constraint type and constructor; document options; add
See ...for specs if applicable.
4. Tests (mandatory)
In test/constraints_*_cases_test.go (create or extend the right file, e.g. constraints_identifiers_cases_test.go):
- Define a slice of
ConstraintValidationTestCase. - Use
name,isApplicableFor: specificValueTypes(stringType)(or other type),stringValue: stringValue("..."),constraint: it.IsMyFormat(),assert: assertNoErrororassertHasOneViolation(validation.ErrInvalidMyFormat, message.InvalidMyFormat). - Cover: valid, invalid, empty/nil (if applicable), options (e.g. WithError/WithMessage), When(false)/When(true).
Add the slice to validateTestCases in test/constraints_test.go via mergeTestCases(...) so the shared test runners pick it up.
5. Examples (mandatory)
In it/example_test.go add testable examples:
go1func ExampleIsMyFormat_valid() { 2 err := validator.Validate(context.Background(), validation.String("valid-value", it.IsMyFormat())) 3 fmt.Println(err) 4 // Output: 5 // <nil> 6} 7 8func ExampleIsMyFormat_invalid() { 9 err := validator.Validate(context.Background(), validation.String("invalid", it.IsMyFormat())) 10 fmt.Println(err) 11 // Output: 12 // violation: "This value is not a valid my format." 13}
Use // Output: so go test runs them. Prefer ExampleXxx_valid / ExampleXxx_invalid naming.
6. Optional: package validate
Add when the constraint is useful for standalone validation (e.g. string codes, identifiers), without the full validator.
- File:
validate/identifiers.goor new file (e.g.validate/myformat.go). - Signature:
func MyFormat(value string) error(or with options). - Return:
nilif valid; otherwise a sentinel error fromvalidatepackage (e.g.ErrTooShort, or custom). - Godoc: Describe when it returns which error.
- Tests:
validate/*_test.gotable or cases. - Examples:
validate/example_test.gowithExampleMyFormatand// Output:.
If the it constraint needs options, define option types and funcs in validate (e.g. validate.MyOptions, validate.AllowXxx()), and use them from it and is.
7. Optional: package is
Add when useful for standalone boolean checks (e.g. in conditions, or for OfStringBy).
- File:
is/identifiers.goor same area as relatedvalidatelogic. - Signature:
func MyFormat(value string) boolorfunc MyFormat(value string, options ...func(o *validate.MyOptions)) bool. - Implementation: Usually
return validate.MyFormat(value, options...) == nil. - Godoc: Short description; point to
validatefor options and semantics. - Tests:
is/*_test.go. - Examples:
is/example_test.gowithExampleMyFormatand// Output:.
Checklist
-
message/messages.go: new constant -
errors.go:ErrXxx = NewError("code", message.Xxx) -
message/translations/english/messages.go: key = message const, value = English -
message/translations/russian/messages.go: key = message const, value = Russian -
it/*.go: constraint (StringFuncConstraint or custom struct), godoc, empty/nil handling, BuildViolation -
test/constraints_*_cases_test.go: test cases + merge intovalidateTestCases -
it/example_test.go: ExampleXxx_valid, ExampleXxx_invalid with// Output: - Optional:
validate: function, tests, examples, godoc - Optional:
is: function, tests, examples, godoc -
go test ./...andgolangci-lint run
Additional resources
- Plural forms and Russian translation details: reference.md
- Existing patterns:
it/identifiers.go(UUID/ULID),it/string.go(OfStringBy),test/constraints_identifiers_cases_test.go,message/translations/.