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):
const ( // ... existing InvalidMyFormat = "This value is not a valid my format." )
- •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:
var (
// ... existing
ErrInvalidMyFormat = NewError("invalid my format", message.InvalidMyFormat)
)
- •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:
var Messages = map[language.Tag]map[string]catalog.Message{
language.English: {
// ... existing
message.InvalidMyFormat: catalog.String(message.InvalidMyFormat),
},
}
- •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:
var Messages = map[language.Tag]map[string]catalog.Message{
language.Russian: {
// ... existing
message.InvalidMyFormat: catalog.String("Значение не является допустимым форматом."),
},
}
- •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:
// IsMyFormat validates whether the value is in my format.
// See [link] for specification.
func IsMyFormat() validation.StringFuncConstraint {
return validation.OfStringBy(is.MyFormat).
WithError(validation.ErrInvalidMyFormat).
WithMessage(validation.ErrInvalidMyFormat.Message())
}
3.2 Custom struct constraint
When you need options (e.g. versions, formats), define a struct and implement ValidateString:
// MyConstraint validates whether the string value satisfies my format.
// Use [MyConstraint.Option] to configure.
type MyConstraint struct {
isIgnored bool
groups []string
options []func(o *validate.MyOptions)
err error
messageTemplate string
messageParameters validation.TemplateParameterList
}
// IsMy creates the constraint.
func IsMy() MyConstraint {
return MyConstraint{
err: validation.ErrInvalidMyFormat,
messageTemplate: validation.ErrInvalidMyFormat.Message(),
}
}
// WithError overrides default error for produced violation.
func (c MyConstraint) WithError(err error) MyConstraint { ... }
// WithMessage sets the violation message template.
func (c MyConstraint) WithMessage(template string, parameters ...validation.TemplateParameter) MyConstraint { ... }
// When / WhenGroups for conditional validation.
func (c MyConstraint) When(condition bool) MyConstraint { ... }
func (c MyConstraint) WhenGroups(groups ...string) MyConstraint { ... }
func (c MyConstraint) ValidateString(ctx context.Context, validator *validation.Validator, value *string) error {
if c.isIgnored || validator.IsIgnoredForGroups(c.groups...) || value == nil || *value == "" {
return nil
}
if is.My(*value, c.options...) {
return nil
}
return validator.BuildViolation(ctx, c.err, c.messageTemplate).
WithParameters(
c.messageParameters.Prepend(
validation.TemplateParameter{Key: "{{ value }}", Value: *value},
)...,
).
Create()
}
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:
func ExampleIsMyFormat_valid() {
err := validator.Validate(context.Background(), validation.String("valid-value", it.IsMyFormat()))
fmt.Println(err)
// Output:
// <nil>
}
func ExampleIsMyFormat_invalid() {
err := validator.Validate(context.Background(), validation.String("invalid", it.IsMyFormat()))
fmt.Println(err)
// Output:
// violation: "This value is not a valid my format."
}
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/.