Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Validation rules

Requires the validate Cargo feature, which is enabled by default.

What kind of configuration crate would this be without built-in validation? As such, we support it as a first-class feature, with built-in validation rules provided by the garde crate.

In Schematic, validation does not happen as part of the serde parsing process, and instead happens for each partial configuration to be merged. However, with that said, prefer serde parsing over validation rules for structural adherence (learn more).

Validation can be applied on a per-setting basis with the #[setting(validate)] attribute field, which requires a reference to a function to call.

#![allow(unused)]
fn main() {
#[derive(Config)]
struct AppConfig {
	#[setting(validate = schematic::validate::alphanumeric)]
	pub secret_key: String,

	#[setting(validate = schematic::validate::regex("^\.env"))]
	pub env_file: String,
}
}

Or on a per-variant basis when using an enum.

#![allow(unused)]
fn main() {
#[derive(Config)]
enum Projects {
	#[setting(validate = schematic::validate::min_length(1))]
	List(Vec<String>),
	// ...
}
}

A variant holding several values passes them as a tuple of references, not as a reference to a tuple.

#![allow(unused)]
fn main() {
fn validate_pair<P, C>(value: (&String, &usize), partial: &P, context: &C, finalize: bool) -> ValidateResult {
	// ...
}

#[derive(Config)]
enum Entry {
	#[setting(validate = validate_pair)]
	Pair(String, usize),
	// ...
}
}

We provide a handful of built-in validation functions in the validate module. Furthermore, some functions are factories which can be called to produce a validator.

Required settings

Every setting in a partial configuration is optional, so a setting that no source provides falls back to its default value, or to Default::default() when it has none. To fail instead, as serde does for a missing field, mark the setting with #[setting(required)].

#![allow(unused)]
fn main() {
#[derive(Config)]
struct AppConfig {
	#[setting(required)]
	pub name: String,

	#[setting(required, env = "APP_TOKEN")]
	pub token: String,

	#[setting(required)]
	pub port: Option<usize>,
}
}

A required setting has no default value, and is checked by the final validation pass, once every source and environment variable has been merged. Any one of them may provide it, so each source is free to omit it. A null counts as missing, while an empty value, like "" or [], does not.

When the setting is wrapped in Option, the value must be Some, but the field stays an Option in the final configuration. A setting that isn’t wrapped still has to implement Default, which AppConfig::default() falls back to.

A required setting can’t declare a default, as it would always have a value. Nor can a nested config that isn’t wrapped in Option, as one always exists in the final configuration. Mark the settings within it as required instead, which are then checked even when no source provided the nested config at all.

Validate handler function

You can also define your own function for validating values, also known as a validator.

When defining a custom validate function, the value to check is passed as the first argument, the current/parent partial as the second, the context as the third, and whether this is the final validation pass.

#![allow(unused)]
fn main() {
fn validate_string(
	value: &str,
	partial: &PartialAppConfig,
	context: &Context
	finalize: bool
) -> ValidateResult {
	if !do_check(value) {
		return Err(ValidateError::new("Some failure message"));
	}

	Ok(())
}
}

If validation fails, you must return a ValidateError with a failure message.

The setting is passed by reference, and deref coerces on its way in, so the first argument can take a borrowed form of the type instead of the type itself. A String setting reaches a &str validator, as above, and a Vec<T> setting reaches a &[T] one.

Factories

For composition and reusability concerns, we also support factory functions that can be called to create a unique validator. This can be seen above with schematic::validate::regex. To create your own factory, declare a normal function, with any number of arguments, that returns a Validator.

Using the regex factory as an example, it would look something like this. The closure takes the same four arguments a plain validator does.

#![allow(unused)]
fn main() {
use schematic::Validator;

fn regex<T, P, C>(pattern: &str) -> Validator<T, P, C> {
	let pattern = regex::Regex::new(pattern).unwrap();

	Box::new(move |value, _, _, _| {
		if !pattern.is_match(value) {
			return Err(ValidateError::new("Some failure message"));
		}

		Ok(())
	})
}
}

Path targeting

If validating an item in a list or collection, you can specifiy the nested path when failing. This is extremely useful when building error messages.

#![allow(unused)]
fn main() {
use schematic::PathSegment;

ValidateError::with_segments(
	"Some failure message",
	// [i].key
	[PathSegment::Index(i), PathSegment::Key(key.to_string())]
)
}

Context and partial handling

If you’re not using context, or want to create a validator for any kind of partial, we suggest generic inferrence.

#![allow(unused)]
fn main() {
fn using_generics<P, C>(value: &str, partial: &P, context: &C, finalize: bool) -> ValidateResult {
	// ...
}
}

Cargo features

The following Cargo features can be enabled for more functionality:

  • validate_email - Enables email validation with the schematic::validate::email function.
  • validate_url - Enables URL validation with the schematic::validate::url and url_secure functions.