Types of REST Parameters. Validation and Sanitization

Since version 5.2, WordPress has built-in validation and sanitization for parameters passed to REST API routes (parameter type validator). This validation is based on the JSON Schema Version 4. This article will cover the basics of JSON schema used in WordPress for validation and sanitization.

The types of parameters described below are specified when registering a route and its endpoints in the register_rest_route() function.

Let’s look at an example showing how and where the endpoint parameters mentioned below are specified:

$endpoints = [
	[
		'methods'             => 'GET',
		'callback'            => [ $this, 'get_items' ],
		'permission_callback' => [ $this, 'get_items_permissions_check' ],
		'args'                => [
			// string
			'context' => [
				'description'       => __( 'Scope under which the request is made; determines fields present in response.' ),
				'type'              => 'string',
				'sanitize_callback' => 'sanitize_key',
				'validate_callback' => 'rest_validate_request_arg',
			],
			// integer
			'per_page' => [
				'description'       => __( 'Maximum number of items to be returned in result set.' ),
				'type'              => 'integer',
				'default'           => 10,
				'minimum'           => 1,
				'maximum'           => 100,
				'sanitize_callback' => 'absint',
				'validate_callback' => 'rest_validate_request_arg',
			],
			// array
			'author' => [
				'description' => __( 'Limit result set to posts assigned to specific authors.' ),
				'type'        => 'array',
				'items'       => [
					'type' => 'integer',
				],
				'default'     => [],
			],
			// string enum
			'order' => [
				'description' => __( 'Order sort attribute ascending or descending.' ),
				'type'        => 'string',
				'default'     => 'desc',
				'enum'        => [ 'asc', 'desc' ],
			],
			// string pattern
			'slug'   => [
				'type'        => 'string',
				'required'    => true,
				'description' => __( 'WordPress.org plugin directory slug.' ),
				'pattern'     => '[\w\-]+',
			],

		],
	]
];

register_rest_route( 'namespace', '/rest_base', $endpoints );

type

The following seven primitive types are available in JSON schema:

  • string — string value.
  • null — null value.
  • number — Any number. Equivalent to float in PHP.
  • integer — Integer. float is not allowed.
  • boolean — value true/false.
  • array — List of values. Equivalent to an array in JavaScript. In PHP, it corresponds to an array with numeric keys or an array without specified keys.
  • object — Pairs of key => value. Equivalent to an object in JavaScript. In PHP, it corresponds to an associative array (array with keys).

The primitive type is specified in the type element of the array. For example:

array(
	'type' => 'string',
);

JSON schema allows specifying multiple types at once:

array(
	'type' => [ 'boolean', 'string' ],
);

Type Conversion

The WordPress REST API accepts data in GET or POST requests, so the data needs to be converted. For example, some string values need to be turned into their actual types.

  • string — this type must pass the is_string() check.
  • null — the value must be an actual null. This means that sending a null value in the URL or as a URL-encoded form body is not possible; it must use a JSON request body.
  • number — a number or a string that passes the is_numeric() check. The value will be converted to (float).
  • integer — Integers or strings without a decimal part. The value will be converted to (int).
  • boolean — Logical true/false. Numbers or strings 0, 1, '0', '1', 'false', 'true'. 0 is false. 1 is true.
  • array — Indexed array corresponding to wp_is_numeric_array() or a string. If a string is provided, values separated by commas will become array element values. If there are no commas in the string, the value will become the first element of the array. For example: 'red, yellow' will turn into array( 'red', 'yellow' ), while 'blue' will become array( 'blue' ).
  • object — Array, stdClass object, object implementing JsonSerializable, or an empty string. Values will be converted to a PHP array.

When using multiple types, the types will be processed in the specified order. This may affect the sanitization result. For example, for 'type' => [ 'boolean', 'string' ] the sent value '1' will turn into the boolean true. However, if the order is changed, the value will be a string '1'.

The JSON schema specification allows defining schemas without the type field. But in WordPress, this parameter must be specified; otherwise, you will receive a notice _doing_it_wrong().

string

Indicates that the value of the request should be a string. Additional parameters below can define what kind of string should be passed in the parameter value.

minLength / maxLength

Used to limit the allowable length of the string (inclusive). It’s important to note that multibyte characters count as one character, and the boundaries are included in the count.

For example, for the following schema, ab, abc, abcd will pass validation, while a and abcde will not.

array(
	'type' => 'string',
	'minLength' => 2,
	'maxLength' => 4,
);

format

If this argument is specified, the values of the parameter passed in REST will be checked against the specified format.

Possible format options:

Example of using the format parameter:

array(
	'type'   => 'string',
	'format' => 'date-time',
);

Important: the parameter value must always match the specified format, so it cannot be an empty string. If the ability to specify an empty string is needed, the type null should be added.

For example, the following schema will allow specifying an IP (127.0.0.1) or not specifying the parameter value:

array(
	'type'   => [ 'string', 'null' ],
	'format' => 'ip',
);

The core code that shows how this parameter specifically works:

// This behavior matches rest_validate_value_from_schema().
if ( isset( $args['format'] )
	&& ( ! isset( $args['type'] ) || 'string' === $args['type'] || ! in_array( $args['type'], $allowed_types, true ) )
) {
	switch ( $args['format'] ) {
		case 'hex-color':
			return (string) sanitize_hex_color( $value );

		case 'date-time':
			return sanitize_text_field( $value );

		case 'email':
			// sanitize_email() validates, which would be unexpected.
			return sanitize_text_field( $value );

		case 'uri':
			return esc_url_raw( $value );

		case 'ip':
			return sanitize_text_field( $value );

		case 'uuid':
			return sanitize_text_field( $value );
	}
}

Note that format is processed not only when type = string. format will be applied if:

  • type = string.
  • type differs from the standard primitive type.
  • type is not specified (but this is prohibited in WP).

pattern

Used to check if the string parameter value matches the specified regular expression.

For example, for the following schema, #123 fits, while #abc does not:

array(
	'type'    => 'string',
	'pattern' => '#[0-9]+',
);

Regular expression modifiers are not supported, i.e., you cannot specify /i to be case insensitive.

The JSON RFC schema recommends limiting to the following regular expression functions to ensure compatibility with as many different programming languages as possible:

  • Individual Unicode characters corresponding to the JSON specification [RFC4627] are allowed.
  • Simple character groups and ranges: [abc] and [a-z].
  • Simple groups and ranges of excluding characters: [^abc], [^a-z].
  • Simple quantifiers: * (zero or more), + (one or more), ? (one or none), and their "lazy" versions: +?, *?, ??.
  • Range quantifiers: {x} (x times), {x,y} (from x to y times), {x,} (x and more times), and their "lazy" versions.
  • Anchors for the start and end of the string: ^, $.
  • Simple groups (...) and alternation |.

The pattern must be compatible with the ECMA 262 regular expression dialect.

null

The value must be an actual null. This means that sending a null value in the URL or as a URL-encoded form body is not possible; it must use a JSON request body.

boolean

Logical true/false. You can specify numbers or strings in the request:

  • true — 1, '1', 'true'.
  • false — 0, '0', 'false'.

For more information on how the passed value is processed, see the code of the function: rest_sanitize_boolean().

number / integer

Numbers have the type number (any number, can be fractional) or integer (only whole numbers):

if ( 'integer' === $args['type'] ) {
	return (int) $value;
}

if ( 'number' === $args['type'] ) {
	return (float) $value;
}

For numbers, there are also additional validation parameters.

minimum / maximum

Limits the range of acceptable numbers (including the numbers themselves). For example, 2 will fit the schema below, while 0 and 4 will not:

array(
	'type' => 'integer',
	'minimum' => 1,
	'maximum' => 3,
);

exclusiveMinimum / exclusiveMaximum

These are additional parameters for minimum / maximum that disable "inclusive" checking. That is, the value CANNOT equal a specified minimum or maximum but must be greater or less than, but not equal.

For example, in the following case, the only acceptable value will be 2:

array(
	'type'             => 'integer',
	'minimum'          => 1,
	'exclusiveMinimum' => true,
	'maximum'          => 3,
	'exclusiveMaximum' => true,
);

multipleOf

Affirms the multiplicity of the number, i.e., the resulting value must be evenly divisible by the number specified in this parameter.

For example, the following schema will only accept even integers:

array(
	'type'       => 'integer',
	'multipleOf' => 2,
);

Decimal fractions are also supported. For example, the following schema can be used to accept a percentage with a maximum value of one-tenth:

array(
	'type'       => 'number',
	'minimum'    => 0,
	'maximum'    => 100,
	'multipleOf' => 0.1,
);

array

Indicates that an array must be passed. See: rest_sanitize_array().

items

Sets the format for each element in the array. To do this, you need to use the items parameter, in which you need to specify the JSON schema for each array element.

For example, the following schema requires an array of IP addresses:

array(
	'type'  => 'array',
	'items' => array(
		'type'   => 'string',
		'format' => 'ip',
	),
);

For such a schema, this data will pass validation:

[ "127.0.0.1", "255.255.255.255" ]

And this will not:

[ "127.0.0.1", 5 ]

The items schema can be anything, including a nested array schema:

array(
	'type'  => 'array',
	'items' => array(
		'type'  => 'array',
		'items' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

For such a schema, this data will pass validation:

[
  [ "#ff6d69", "#fecc50" ],
  [ "#0be7fb" ]
]

And this will not:

[
  [ "#ff6d69", "#fecc50" ],
  "george"
]

minItems / maxItems

Used to limit the minimum and maximum number of elements in the array (inclusive).

For example, the following schema will allow the data [ 'a' ] and [ 'a', 'b' ], but will not allow
[] and [ 'a', 'b', 'c' ]:

array(
	'type'     => 'array',
	'minItems' => 1,
	'maxItems' => 2,
	'items'    => array(
		'type' => 'string',
	),
);

uniqueItems

Used when you need the values in the array to be unique. See: rest_validate_array_contains_unique_items().

For example, the following schema will consider the data [ 'a', 'b' ] correct, and [ 'a', 'a' ] incorrect:

array(
	'type'        => 'array',
	'uniqueItems' => true,
	'items'       => array(
		'type' => 'string',
	),
);
Important to know about uniqueness of values.
  • Values of different types are considered unique. For example, '1', 1 and 1.0 — these are different values.

  • When comparing arrays, the order of elements matters. For example, in such an array, the values will be considered unique:

    [
      [ "a", "b" ],
      [ "b", "a" ]
    ]
  • When comparing objects, the order of property definitions does NOT matter. For example, in such an array, the objects will be considered identical:

    [
      {
    	"a": 1,
    	"b": 2
      },
      {
    	"b": 2,
    	"a": 1
      }
    ]
  • Uniqueness is checked recursively for array values in both functions: rest_validate_value_from_schema() and rest_sanitize_value_from_schema(). This is to ensure that there are no moments when items may be unique before sanitization and identical after.

    For example, take such a schema:

    array(
    	'type' => 'array',
    	'uniqueItems' => true,
    	'items' => array(
    		'type' => 'string',
    		'format' => 'uri',
    	),
    );

    This request would pass validation because the strings are different:

    [ "https://site.com/hello world", "https://site.com/hello%20world" ]

    However, after processing with the esc_url_raw() function, the strings will become identical.

    In this case, rest_sanitize_value_from_schema() would return an error. Therefore, you should always check and sanitize parameters.

object

This type requires that the incoming data be an object. You also need to specify the format for each property of the object in the properties parameter.

See rest_sanitize_object().

properties

Required properties of the object. A schema is specified for each property.

For example, the following schema requires an object where the property name is a string and color is a hex color.

array(
	'type'       => 'object',
	'properties' => array(
		'name'  => array(
			'type' => 'string',
		),
		'color' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

This data will pass validation:

{
  "name": "Primary",
  "color": "#ff6d69"
}

And this will not:

{
  "name": "Primary",
  "color": "orange"
}
Required properties

By default, all properties listed for the object are optional, so if any property is missing in the object, the validation will still pass.

There are two ways to make a property required.

Method 1: add a required field for each property.

This is the syntax of version 3 of the JSON schema:

array(
	'type'       => 'object',
	'properties' => array(
		'name'  => array(
			'type'     => 'string',
			'required' => true,
		),
		'color' => array(
			'type'     => 'string',
			'format'   => 'hex-color',
			'required' => true,
		),
	),
);

Method 2: add a required field in a common array listing the required properties.

This is the syntax of version 4 of the JSON schema:

register_post_meta( 'post', 'fixed_in', array(
	'type'         => 'object',
	'show_in_rest' => array(
		'single' => true,
		'schema' => array(
			'required'   => array( 'revision', 'version' ),
			'type'       => 'object',
			'properties' => array(
				'revision' => array(
					'type' => 'integer',
				),
				'version'  => array(
					'type' => 'string',
				),
			),
		),
	),
) );

Now the following request will fail validation:

{
	"title": "Check required properties",
	"content": "We should check that required properties are provided",
	"meta": {
		"fixed_in": {
			"revision": 47089
		}
	}
}

If the meta-field fixed_in is not specified at all, no error will occur. The object defining the list of required properties does not define the object itself as required. Just if the object is specified, the required properties must also be specified.

Syntax version 4 is not supported for top-level endpoint schemas in WP_REST_Controller::get_item_schema().

If the following schema is specified, the user can send a successful request without specifying the properties title and content. This happens because the schema document itself is not used for validation, but is instead transformed into a list of parameter definitions.

array(
	'$schema'    => 'http://json-schema.org/draft-04/schema#',
	'title'      => 'my-endpoint',
	'type'       => 'object',
	'required'   => array( 'title', 'content' ),
	'properties' => array(
		'title'   => array(
			'type' => 'string',
		),
		'content' => array(
			'type' => 'string',
		),
	),
);

additionalProperties

Defines whether the object can contain additional properties not specified in the schema.

By default, the JSON schema allows specifying properties that are not defined in the schema.

Thus, to ensure the following does not pass validation, you need to set the additionalProperties = false, i.e., unspecified (additional) properties are prohibited.

array(
	'type'                 => 'object',
	'additionalProperties' => false,
	'properties'           => array(
		'name'  => array(
			'type' => 'string',
		),
		'color' => array(
			'type'   => 'string',
			'format' => 'hex-color',
		),
	),
);

The additionalProperties keyword can itself be used as a schema for object properties. Thus, unknown properties must pass the specified check.

This can be useful when you want to accept a list of values, each of which can have its own unique key not described in the schema, but the values must conform to the schema. For example:

array(
	'type'                 => 'object',
	'properties'           => array(),
	'additionalProperties' => array(
		'type'       => 'object',
		'properties' => array(
			'name'  => array(
				'type'     => 'string',
				'required' => true,
			),
			'color' => array(
				'type'     => 'string',
				'format'   => 'hex-color',
				'required' => true,
			),
		),
	),
);

Now the following data will pass validation:

{
  "primary": {
	"name": "Primary",
	"color": "#ff6d69"
  },
  "secondary": {
	"name": "Secondary",
	"color": "#fecc50"
  }
}

But these will not:

{
  "primary": {
	"name": "Primary",
	"color": "#ff6d69"
  },
  "secondary": "#fecc50"
}

patternProperties

The patternProperties keyword is similar to the additionalProperties keyword, but allows asserting that a property matches a regular expression pattern. The keyword is an object where each property is a regular expression pattern, and its value is the JSON schema used to validate properties matching that pattern.

For example, this schema requires that each value be a hex color, and the property must contain only "word" characters.

array(
  'type'                 => 'object',
  'patternProperties'    => array(
	'^\\w+$' => array(
	  'type'   => 'string',
	  'format' => 'hex-color',
	),
  ),
  'additionalProperties' => false,
);

As a result, this will pass validation:

{
  "primary": "#ff6d69",
  "secondary": "#fecc50"
}

And this will not:

{
  "primary": "blue",
  "$secondary": "#fecc50"
}

When validating the patternProperties schema, if a property does not match any of the patterns, that property will be allowed and its content will not be checked in any way. If such behavior is not suitable, you need to prohibit unknown (additional) properties using the additionalProperties parameter.

minProperties / maxProperties

Used to limit the minimum and maximum number of properties of the object (inclusive). These are analogous to the [minItems and maxItems] of an array](#minitems-maxitems).

This can be useful when describing a schema where unknown object properties are allowed, but there must be a limited number of them. For example:

array(
  'type'                 => 'object',
  'additionalProperties' => array(
	'type'   => 'string',
	'format' => 'hex-color',
  ),
  'minProperties'        => 1,
  'maxProperties'        => 2,
);

This data will pass validation:

{
  "primary": "#52accc",
  "secondary": "#096484"
}

But these will not:

{
  "primary": "#52accc",
  "secondary": "#096484",
  "tertiary": "#07526c"
}

enum

Allows specifying a list of possible values for the passed parameter (when the parameter has a limited list of possible values).

This parameter is not dependent on the specified type and will work with any type, as long as the received value is in the specified list of values.

enum is checked only during value validation (validate not sanitize) in the function rest_validate_value_from_schema().

Example of using the parameter:

array(
	'description' => __( 'Order sort attribute ascending or descending.' ),
	'type'        => 'string',
	'default'     => 'asc',
	'enum'        => array(
		'asc',
		'desc',
	),
);

The core code shows how the check is actually performed:

if ( ! in_array( $value, $args['enum'], true ) ) {
	return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
}

oneOf / anyOf

Matches one or any of the described schemas. See: rest_find_any_matching_schema(), rest_find_one_matching_schema().

These are extended keywords that allow the JSON schema validator to choose one of many schemas to use for value validation.

  • anyOf allows the value to match one of the specified schemas or multiple schemas.
  • oneOf requires that the value matches only one schema (not two or more).

For example, this schema allows sending an array of "operations" to the endpoint. Each operation can be either "crop" or "rotation".

array(
	'type'  => 'array',
	'items' => array(
		'oneOf' => array(
			array(
				'title'      => 'Crop',
				'type'       => 'object',
				'properties' => array(
					'operation' => array(
						'type' => 'string',
						'enum' => array(
							'crop',
						),
					),
					'x'         => array(
						'type' => 'integer',
					),
					'y'         => array(
						'type' => 'integer',
					),
				),
			),
			array(
				'title'      => 'Rotation',
				'type'       => 'object',
				'properties' => array(
					'operation' => array(
						'type' => 'string',
						'enum' => array(
							'rotate',
						),
					),
					'degrees'   => array(
						'type'    => 'integer',
						'minimum' => 0,
						'maximum' => 360,
					),
				),
			),
		),
	),
);

The REST API will loop through each schema specified in the oneOf array and check for a match. If only one schema matches, the validation will be successful. If multiple schemas match, the validation will fail. If none of the schemas match, the validator will attempt to find the closest matching schema and return an appropriate error message.

operations[0] is not a valid Rotation. Reason: operations[0][degrees] must be between 0 (inclusive) and 360 (inclusive)

To generate understandable error messages, it is recommended to assign each schema in the oneOf or anyOf array a title property.

How Validation and Sanitization Work

In the REST API, two basic functions are defined for working with JSON schema:

Both functions take a value that needs to be validated/sanitized, the parameter schema, and the parameter name (the name will be displayed in the error message).

These functions should be used in strict order: first validate the value (validate) and only then sanitize it (sanitize). If any of the checks are not used, the endpoint data may not be adequately protected.

Registering Endpoint Parameters and Validation/Sanitization

Let’s consider an example of how this works. Suppose we register routes in the controller as follows:

class WP_REST_Terms_Controller extends WP_REST_Controller {

	...

	public function register_routes() {

		register_rest_route( 'my/v1', '/myelem',
			array(
				array(
					'methods'             => 'GET',
					'callback'            => array( $this, 'get_items' ),
					'permission_callback' => array( $this, 'get_items_permissions_check' ),
					'args'                => $this->get_collection_params(),
				),
				array(
					'methods'             => 'POST, PUT',
					'callback'            => array( $this, 'create_item' ),
					'permission_callback' => array( $this, 'create_item_permissions_check' ),
					'args'                => $this->get_endpoint_args_for_item_schema( 'POST, PUT' ),
				),
				'schema' => array( $this, 'get_public_item_schema' ),
			)
		);

		...
	}

	...
}
If the endpoint parameters fully correspond to the resource schema

Then to register the possible parameters of the endpoint, you can use the method WP_REST_Controller::get_endpoint_args_for_item_schema(). In this case, the validation/sanitization callbacks will be added automatically.

Here’s how it works:

public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {
	return rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method );
}

The function rest_get_endpoint_args_for_schema() receives the schema $this->get_item_schema() and based on it compiles a list of available parameters, into which validation and sanitization callbacks are added:

$endpoint_args[ $field_id ] = array(
	'validate_callback' => 'rest_validate_request_arg',
	'sanitize_callback' => 'rest_sanitize_request_arg',
);
If the endpoint parameters do not correspond to the resource schema

Then the sanitization and validation callbacks can be specified manually or not specified at all.

If the sanitization callback is not specified in the sanitize_callback parameter of a specific request parameter, it will automatically be added by the method WP_REST_Request::sanitize_params(), which is called from WP_REST_Server::dispatch() when processing the request data.

For example:

$query_params['author_email'] = array(
	'type'        => 'string',
	'format'      => 'email',
	'description' => 'Parameter Description.',
);

// the same as
$query_params['author_email'] = array(
	'type'        => 'string',
	'format'      => 'email',
	'description' => 'Parameter Description.',
	'sanitize_callback' => 'rest_parse_request_arg',
);

The core code that shows how the sanitization callback is added:

// If the arg has a type but no sanitize_callback attribute, default to rest_parse_request_arg.
if ( ! array_key_exists( 'sanitize_callback', $param_args ) && ! empty( $param_args['type'] ) ) {
	$param_args['sanitize_callback'] = 'rest_parse_request_arg';
}

Code of rest_parse_request_arg():

function rest_parse_request_arg( $value, $request, $param ) {
	$is_valid = rest_validate_request_arg( $value, $request, $param );

	if ( is_wp_error( $is_valid ) ) {
		return $is_valid;
	}

	$value = rest_sanitize_request_arg( $value, $request, $param );

	return $value;
}

Note that before sanitizing the value, a check is performed. Therefore, there is no need to specify the validate_callback argument when using the rest_parse_request_arg() function for sanitization.

However, if you use another callback for sanitization that does not perform validation, it is better to specify the value check separately in the validate_callback argument. For example:

$query_params['page'] = array(
	'description'       => __( 'Current page of the collection.' ),
	'type'              => 'integer',
	'default'           => 1,
	'sanitize_callback' => 'absint',
	'validate_callback' => 'rest_validate_request_arg',
	'minimum'           => 1,
),

--

https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#json-schema-basics