- Overview
- Quick Start
- Introduction
- Guides
- Client Libraries
- API Reference
- Examples
- DDD Resources
- Validation user guide
- Validation developer guide
Implement a validator
To validate a Protobuf message type M with custom logic:
- Implement
io.spine.validation.MessageValidator<M>. - Make the implementation discoverable via Java
ServiceLoader(recommended), or register it inValidatorRegistryexplicitly. - Ensure the class has a public no-args constructor.
Keep validators stateless and cheap to construct.
Reference implementation: TimestampValidator
Let’s review the MessageValidator implementation on the example of
io.spine.validation.TimestampValidator from the Validation JVM runtime.
It validates com.google.protobuf.Timestamp and reports violations for invalid
seconds and nanos values.
Service discovery
The validator is a regular MessageValidator<Timestamp> implementation and is discoverable via
ServiceLoader.
To generate the required service provider configuration automatically, annotate it with
@AutoService(MessageValidator::class):
import com.google.auto.service.AutoService
import io.spine.validation.MessageValidator
@AutoService(MessageValidator::class)
public class TimestampValidator : MessageValidator<Timestamp> {
// ...
}
Validation logic
The core logic is intentionally small: it first delegates to Timestamps.isValid(message) and,
if invalid, adds a field-specific violation for each invalid field (seconds and/or nanos).
For range checks, it relies on Timestamps.MIN_VALUE and Timestamps.MAX_VALUE.
@AutoService(MessageValidator::class)
public class TimestampValidator : MessageValidator<Timestamp> {
override fun validate(message: Timestamp): List<DetectedViolation> {
if (Timestamps.isValid(message)) {
return emptyList()
}
val violations = mutableListOf<DetectedViolation>()
if (message.seconds < MIN_VALUE.seconds ||
message.seconds > MAX_VALUE.seconds) {
violations.add(invalidSeconds(message.seconds))
}
if (message.nanos !in 0..MAX_VALUE.nanos) {
violations.add(invalidNanos(message.nanos))
}
return violations
}
}
The code snippet above omits import statements and helper functions for brevity. You can find the full implementation via GitHub.
Reporting violations with placeholders
TimestampValidator reports errors via FieldViolation, providing:
fieldPath— which field is invalid, for example,"seconds",fieldValue— the actual invalid value, andmessage— aTemplateStringwith placeholders and a placeholder-to-value map.
The message is defined as a template (via withPlaceholders) and populated by specifying
values in placeholderValue. This keeps error messages machine-friendly and allows consistent
formatting, logging, and customization.
When violations are converted to regular ConstraintViolations, Spine Validation also populates
the validator placeholder with the fully qualified class name of the validator.
Below is the helper that creates a violation for invalid seconds.
private fun invalidSeconds(seconds: Long): FieldViolation = FieldViolation(
message = templateString {
withPlaceholders =
"The ${FIELD_PATH.value} value is out of range" +
" (${RANGE_VALUE.value}): $seconds."
placeholderValue.put(FIELD_PATH.value, "seconds")
placeholderValue.put(RANGE_VALUE.value,
"${MIN_VALUE.seconds}..${MAX_VALUE.seconds}")
},
fieldPath = fieldPath {
fieldName.add("seconds")
},
fieldValue = seconds
)
The invalidNanos() function is similar.
Walkthrough: validate a nested message field
To validate a message nested inside another message, mark the field with (validate) = true.
This applies both generated constraints (if any) and validators registered for the nested type.
Suppose your local model uses Timestamp:
import "google/protobuf/timestamp.proto";
import "spine/options.proto";
message Meeting {
google.protobuf.Timestamp starts_at = 1 [(validate) = true];
}
Once you add a validator for Timestamp, validation of Meeting
reports a violation for the starts_at field if the timestamp is invalid pointing
to the nested field in error (for example, to starts_at.seconds).