Browse all documentation

Custom validation

Add your own rules to a field and show useful messages when a value cannot be saved.

Use .Validate(...) when a field needs a rule beyond built-in options such as .Required() or .MinLength(...). A validator checks a value before Ridu saves it and returns messages the author can act on. The same rule applies to saves from the admin, REST, the SDK, and the Go API.

For messages while someone is typing, see Live server validation. Register both .Validate(...) and .LiveValidate(...) when the rule should run at both times.

Operations and callbacks explains operation.Value[T], operation.Issue, and the context passed to your validator, including what an empty value means.

Add a rule to a field

This example accepts only links that start with https://. Create content/links.go:

content/links.go
package content

import (
  "strings"

  "github.com/riducms/ridu/field"
  "github.com/riducms/ridu/operation"
)

var Link = field.Text("url").Required().Validate(validateHTTPS)

func validateHTTPS(
  // This rule only needs the value, so Go's _ ignores the context.
  _ operation.ValidationContext,
  value operation.Value[string],
) ([]operation.Issue, error) {
  url, present := value.Get()
  // Leave missing or empty values to Required; HTTPS values pass.
  if !present || url == "" || strings.HasPrefix(url, "https://") {
    return nil, nil
  }
  // Return a message the author can fix beside this field.
  return []operation.Issue{{
    Code:    "https_required",
    Message: "Use a URL that starts with https://",
  }}, nil
}

Add Link to your collection’s Fields list. Ridu gives a text validator an operation.Value[string]; Get() returns the string and whether a value is present. This rule leaves empty values to .Required() and checks the prefix when a URL is supplied. It does not check whether the destination exists.

Run bun run dev, open the collection, and save a document with http://example.com as its URL. The form should show “Use a URL that starts with https://”. Change it to https://example.com and save again. The rule should pass.

You can reuse Link inside a Group, Array, or Block. The validator still receives that URL’s value, and Ridu displays its message beside the correct input.

Return a useful message

Return nil, nil when the value passes. Otherwise, return one or more operation.Issue values:

  • Code is a short, stable name that API clients can recognize, such as https_required.
  • Message tells the author what to fix.
  • Target is optional. Leave it out to put the message on this field.

The separate error return is for a failure that prevents the check from running, such as an unavailable external service. Use an issue for an invalid value the author can correct.

Save validators receive the values that would be saved, after field transformations and after an update has been combined with the existing document. Each validator runs once per operation attempt. It can read other values through its context; see Using other field values.

To change a value before saving, use a field hook. Validators report problems; they do not change the document.

Put a message on a child field

Attach a validator directly to the child when the rule only concerns that child. A validator on a Group, Array, or Blocks field is useful when the rule compares several children or rows. Its Target can identify the child that needs attention.

For example, a rule on the details group can point to its nested SEO title:

go
operation.Issue{
  Code:    "title_required",
  Message: "Add an SEO title before publishing",
  // Start inside the group this validator is attached to.
  Target: operation.At("seo.title"),
}

The target starts inside details, so this message belongs to details.seo.title. The seo object must exist. If the object itself is missing or invalid, use operation.At("seo") to put the message on the group instead.

Point to an array or block row

Use the row’s _key when a rule on a container needs to identify a row. Ridu gives each row a key so the message can follow it when rows are reordered.

Field being validated Target Where the message appears
sections array operation.At().Row(sectionKey).Field("heading") The chosen section’s heading
sections array, with nested links operation.At().Row(sectionKey).Field("links").Row(linkKey).Field("url") The chosen link’s URL
layout blocks operation.At().Block(heroKey, "hero").Field("heading") The heading in the chosen Hero block

Read keys from the rows the validator receives, including newly added rows. Do not use an array index as a key. Block also takes the expected block type, such as "hero". Use Field for a direct child or a dotted path through groups; use Row or Block each time the path enters a repeated list.

Ridu reports an error if a target names an unknown child, a missing or duplicate row key, or a different block type. Leave Target unset to put the message on the container itself. The admin keeps server messages on unchanged values after reordering and removes messages whose value or row has changed.

Validate translations and rich-text blocks

A validator editing a French value already works in French. Use the same targets as above; you do not need to include a locale.

If the validator’s value contains all translations, select one explicitly: operation.At("copy").Locale("fr").Field("title"). That translation must exist. A target cannot request a fallback translation or select a different locale from a callback already working in one exact locale.

Arrays inside rich-text blocks use the same targets. Ridu supplies the surrounding rich-text location, so your validator does not need to know how the editor stores its document.

For target methods and types, see the operation API reference.

Get feedback before saving

Use .LiveValidate(...) when an author should see a server check while editing. Keep .Validate(...) registered to enforce the rule on every save. Live feedback does not disable Save or replace save validation.

The Live server validation guide walks through a complete supplier/SKU example and explains when the admin checks a field.

Read unsaved input

Live checks receive operation.LiveValidationContext: values may be incomplete, and defaults and save hooks have not run. See callback arguments.

Understand when feedback appears

Checks start after editing a field and wait 500 ms after the last edit. See timing and feedback for retries, skipped checks, and how Save handles pending work.

Validate embedded fields

Rich-text blocks and plugin embedded forms also support live checks. See rows and embedded forms for context and Apply behavior.

Build a custom client

The admin handles requests automatically. For a separate application, see the SDK example and permissions and limits.

Validate a text or number list

TextList and NumberList validators receive the whole list as operation.Value[[]string] or operation.Value[[]float64]. Use them for rules that compare items, such as rejecting duplicate tags. Item length and number bounds can use the built-in field options instead.

Return an issue on the list field. Items do not have their own issue targets or stable row keys. A position named in a message refers to the submitted order, starting at 1.