Skip to content

The WordPress REST API’s Undocumented Validation Options

The WordPress REST API is still very new. As with many early early technologies, documentation seems to be the last thing to get updated. This is why it can be fun/educational to read through the source code in open source projects. You’ll find things the documentation didn’t teach you! (Or at least not in any documentation I could easily find.)

The custom endpoint I was creating required quite a few arguments when called. I wanted to include sanitization and/or validation of these arguments. Setting up a schema for the endpoint made sense. I knew I could perform validation by specifying a ‘validate_callback’ function in the argument definition.

However browsing the source code in WordPress’ core revealed a few bonuses built right into the core, in a function called rest_validate_value_from_schema.

The ‘type’ attribute

By including in a ‘type‘ attribute when defining an argument, the REST API’s validation will automatically check if the value passed into this argument is of the appropriate data type.

register_rest_route( 'route/v1', '/endpoint/', array(
	'methods' => 'POST',
	'callback' => array( $this, 'endpoint_post_handler' ),
	'permissions_callback' => 'is_user_logged_in',
	'args' => array(
		'first_name' => array(
			'required' => true,
			'type' => 'string',
			'description' => 'The client\'s first name',
		),
	)
) );

In this example above, the “first_name” argument is required, and must be a string. ‘type‘ values that are supported in WordPress 4.7.2 are:

  • array
  • boolean
  • integer
  • number
  • string

NOTE: I could not get the ‘array’ type working properly in my tests. Once I figure out how to get this validation working properly, I’ll update this post.

If the value being passed to the REST endpoint for that argument does not match the data type specified by the type attribute, an HTTP 400 (Bad Request) error will be returned. Data about the invalid argument is returned in the body as part of the JSON response. The screenshot below shows an example of what happens if you pass a string into an argument with type set to boolean:

Sending a string to a boolean argument. (Screenshot from Postman)

The ‘enum’ attribute

The ‘enum‘ attribute allows you to specify a list of valid values for this argument as an array. This is called an enumeration or an enumerated type.

register_rest_route( 'route/v1', '/endpoint/', array(
	'methods' => 'POST',
	'callback' => array( $this, 'endpoint_post_handler' ),
	'permissions_callback' => 'is_user_logged_in',
	'args' => array(
		 'language_preference' => array(
			'required' => true,
			'type' => 'string',
			'description' => 'The locale in which this user would like to receive communications',
			'enum' => array( 
                             'en_CA',
                             'en_US',
                             'fr_CA',
                        ),
		),
	)
) );

In the example above, my language_preference endpoint is a string that only allows a predefined set of locale codes. If the value passed into the argument is not one of these items, an HTTP 400 error is returned. Data about the invalid argument is returned in the body as part of the JSON response.

The values are case-sensitive.

The ‘enum‘ attribute works in conjunction with the ‘type‘ attribute.

The ‘format’ attribute

The ‘format‘ attribute allows you to enforce the format of a string. Accepted values are:

  • date-time
  • email
  • ip
  • uri

For example, the following endpoint must contain an email address:

register_rest_route( 'route/v1', '/endpoint/', array(
	'methods' => 'POST',
	'callback' => array( $this, 'endpoint_post_handler' ),
	'permissions_callback' => 'is_user_logged_in',
	'args' => array(
		 'email' => array(
			'required' => true,
			'type' => 'string',
			'description' => 'The user\'s email address',
			'format' => 'email'
		),
	)
) );

If this were not an email address, an HTTP 400 error is returned. Data about the invalid argument is returned in the body as part of the JSON response.

The date value ensures that the argument contains an RFC3339 timestamp.

The ip value accepts both IPv4 and IPv6 addresses.

The uri value is a sanitization function. It will run the value through the esc_url_raw function to format it as a URI.

Numeric Ranges

There are a few attributes you can assign your endpoints arguments that will be used to check the value of both integer and numeric values. ( Setting the 'type' attribute to integer or number is required). These attributes are:

  • minimum
  • maximum
  • exclusiveMinimum
  • exclusiveMaximum

The minimum and maximum attributes contain the minimum and maximum values that will be accepted for this argument. The exclusiveMinimum and exclusiveMaximum attributes, when present, specify that the value must be greater less or less the specified number, but can’t be that number.

For example, the following endpoint accepts a ‘price’ argument with an integer value between 0 and 1000 (inclusive). Any value between 0 and 1000, including 0 or 1000, will be accepted.

register_rest_route( 'route/v1', '/endpoint/', array(
	'methods' => 'POST',
	'callback' => array( $this, 'endpoint_post_handler' ),
	'permissions_callback' => 'is_user_logged_in',
	'args' => array(
		 'price' => array(
			'required' => true,
			'type' => 'integer',
			'description' => 'Product value',
			'minimum' => 0,
			'maximum => 1000,
		),
	)
) );

The following endpoint accepts a ‘zero_one_two’ argument with an integer value between 0 and 3 (exclusive). This means that values 0, 1 and 2 will be accepted, but 3 will not.

register_rest_route( 'route/v1', '/endpoint/', array(
	'methods' => 'POST',
	'callback' => array( $this, 'endpoint_post_handler' ),
	'permissions_callback' => 'is_user_logged_in',
	'args' => array(
		 'zero_one_two' => array(
			'required' => true,
			'type' => 'integer',
			'description' => 'Product value',
			'minimum' => 0,
                        'maximum' => 3
			'maximumExclusive => true,
		),
	)
) );

This would be the same as using a minimum of 0 and a maximum of 2. The exclusiveMinimum and exclusiveMaximum attributes offer flexibility in how you define these ranges, and the wording of the returned error when a value falls outside these ranges.

Have fun!

These additional options should help you write better REST endpoints with less code than custom validation and sanitization functions.

Have fun!

11 Comments

  1. Thanks for this post! It seems that the type value is case-sensitiv and ‘Array’ does the trick.

  2. Thank you, very helpful! I still can’t see any of this documented in the official handbook so this is a really helpful resource. Thanks for putting it together.

    • Shawn Hooper Shawn Hooper

      You’re welcome Andy. Glad you found it useful!

  3. Julian Julian

    I got here because I was having trouble with the array type too. Looks like it is still buggy.

  4. Julian Julian

    After posting my last comment, I had a face-palm moment and realized how the array type is supposed to work.

    The reason it doesn’t work is because you have to tell it what kind of array you expect and define its schema. Array of strings? objects? numbers?

    So when you declare an array type, just add another “items” key with:
    “items” => [
    “type” => “string”, // or put whatever type you need here
    ]

  5. Dylan Dylan

    Thanks for this post. After a bit of trial and error I found you can use the array type like so:

    ‘colors’ => [
    ‘type’ => ‘array’,
    ‘items’ => [
    ‘type’ => ‘string’,
    ‘enum’ => [
    ‘Red’,
    ‘Green’,
    ‘Blue’,
    ],
    ],
    ],

  6. Thanks, I keep referring to this post every now and again. Just a note about arrays:

    ‘type’ => ‘array’ only seems to work for “regular” “non-associative” arrays if an ‘items’ is passed along with it.

    For associative arrays, I’ve found ‘type’ => ‘object’ works great.

    Let me know what you think!

  7. Thank you so much for this! Exactly what I was looking for!

  8. You can also nest arrays, validate items within associative arrays, and set a number of ‘maxItems’ for an array like so:

    ‘args’ => array(
    ‘validation’ => array(
    ‘type’ => ‘string’,
    ‘enum’ => array( ‘require-all-validate’, ‘normal’ ),
    ‘default’ => ‘normal’,
    ),
    ‘requests’ => array(
    ‘required’ => true,
    ‘type’ => ‘array’,
    ‘maxItems’ => $this->get_max_batch_size(),
    ‘items’ => array(
    ‘type’ => ‘object’,
    ‘properties’ => array(
    ‘method’ => array(
    ‘type’ => ‘string’,
    ‘enum’ => array( ‘POST’, ‘PUT’, ‘PATCH’, ‘DELETE’ ),
    ‘default’ => ‘POST’,
    ),
    ‘path’ => array(
    ‘type’ => ‘string’,
    ‘required’ => true,
    ),
    ‘body’ => array(
    ‘type’ => ‘object’,
    ‘properties’ => array(),
    ‘additionalProperties’ => true,
    ),
    ‘headers’ => array(
    ‘type’ => ‘object’,
    ‘properties’ => array(),
    ‘additionalProperties’ => array(
    ‘type’ => array( ‘string’, ‘array’ ),
    ‘items’ => array(
    ‘type’ => ‘string’,
    ),
    ),
    ),
    ),
    ),
    ),
    )

    I’m also curious about the ‘additionalProperties’ parameter but I haven’t used it yet. There’s still a lot left to explore here!

  9. It looks like there may also be options for a “validate_callback” and a “sanitize_callback” on individual JSON parameters.

    For example:
    ‘args’ => array(
    ‘key’ => array(
    ‘required’ => true,
    ‘type’ => ‘string’,
    ‘sanitize_callback’ => array( ‘Akismet_REST_API’, ‘sanitize_key’ ),
    ‘description’ => __( ‘A 12-character Akismet API key. Available at akismet.com/get/’, ‘akismet’ ),
    ),
    ),

    See the code block here for an example of the validate callback: https://developer.wordpress.org/rest-api/requests/#internal-requests

    And see this source code from Akismet for an example of the sanitize callback: https://plugins.trac.wordpress.org/browser/akismet/tags/4.1.10/class.akismet-rest-api.php#L26

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.