go-unifi

Validation

Control request-body validation modes (soft, hard, disabled), inspect ValidationError, and register custom regex validators.

Before sending a write request, the client validates the request body against the go-playground/validator struct tags carried by the generated resource types (e.g. Network.Purpose only accepts corporate, guest, wan, …). This catches malformed payloads on your side instead of as an opaque controller error. How a failure is handled depends on the validation mode.

Validation only runs on request bodies (the reqBody of POST/PUT/PATCH calls). Read calls (Get, List…) are never validated, and a nil body is skipped.

Validation modes

Set the mode with ClientConfig.ValidationMode:

Prop

Type

hard-validation.go
c, err := unifi.NewClient(&unifi.ClientConfig{
	URL:            "https://unifi.example.com",
	APIKey:         "your-api-key",
	ValidationMode: unifi.HardValidation,
})

The default SoftValidation is deliberate: the generated tags come from the controller's own API definitions and can be stricter than a given controller version actually enforces. Soft mode keeps you moving (and logs a warning you can act on), while hard mode is best in tests and CI where you want a fast, local failure.

Handling a ValidationError

In HardValidation mode a failed validation surfaces as a *unifi.ValidationError, wrapped with context. Use errors.As to reach it and read the per-field messages from Messages:

handle-validation-error.go
n := &unifi.Network{
	Name:     "my-network",
	Purpose:  "invalid-purpose", // not one of the allowed values
	IPSubnet: "10.0.0.10/24",
}

_, err := c.CreateNetwork(ctx, "default", n)
if err != nil {
	var verr *unifi.ValidationError
	if errors.As(err, &verr) {
		for field, msg := range verr.Messages {
			log.Printf("validation error on %s: %s", field, msg)
		}
		return verr
	}
	return err
}

ValidationError has two fields:

  • Messages map[string]string — a human-readable, translated message per failing field (keys are sorted when rendered via Error(), so the string form is deterministic).
  • Root error — the underlying cause. For a normal struct failure this is the raw validator.ValidationErrors, so errors.As(err, &validationErrs) works if you need each field's tag, kind, and value. (On a non-struct or nil argument it instead carries the validator's InvalidValidationError.) Unwrap() exposes Root, so errors.Is/errors.As walk straight through.

Custom validators

The client registers a couple of built-in regex tags (w_regex, numeric_nonzero) that the generated structs reference. To add your own tag, build one with NewCustomRegexValidator and register it through ClientConfig.CustomValidators:

custom-validator.go
// Register a new "hostname_rfc1123" tag usable on request-body struct fields.
hostname := unifi.NewCustomRegexValidator("hostname_rfc1123", `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)

c, err := unifi.NewClient(&unifi.ClientConfig{
	URL:              "https://unifi.example.com",
	APIKey:           "your-api-key",
	CustomValidators: []unifi.CustomValidator{hostname},
})

NewCustomRegexValidator(tag, regex) returns a CustomValidator whose tag becomes a go-playground/validator tag name. Any struct field carrying validate:"...,hostname_rfc1123" is then checked against the pattern, and a failure produces the message <field> must comply with the regular expression pattern '<regex>'. Each custom validator is registered on the client's own validator instance — it does not mutate global state, so two clients can carry different sets.

Custom validators only fire during the client's own request-body validation, on fields whose struct tags reference your tag. The generated resource types are not editable (they are regenerated) and there is no public accessor to the client's validator, so this is most useful for tags the generated definitions already emit but the built-in set does not yet cover.

Next steps

On this page