Creating a Route Schema

A schema in REST API is a complete description of a route; it tells us everything about the route. For more details on what a Schema is, read in another section of this guide, and here we will look at how to create a schema when a custom route is created.

JSON Schema

The JSON schema format for REST is a separate development, which has documentation and the website json-schema.org.

If a simple JSON response is returned to the request, Clients (route users) will not know what specific data they are receiving (it’s good if they are intuitively clear, but this is not always the case). By using a schema, we simplify the understanding of data for clients and also improve our codebase. The schema will help better structure the data so that applications can more easily "reason" about interactions with the REST API. Additionally, having a schema simplifies testing and allows for detection (in some cases).

You can create routes without describing a Schema, but in this case, much will be unclear for the Client, so when creating a route, it is recommended to create (describe) its schema!

Creating a schema may seem like a silly task and some unnecessary work, but if you are creating discoverable and easily extensible endpoints, using a schema is simply necessary!

The route schema consists of a "Resource Schema" and an "Endpoint Schema" (see more). Below we will look at how to create each of these schemas.

Schema Structure

The basic structure of a schema contains just a few elements.

  • $schema — A link to the schema documentation. For example: http://json-schema.org/draft-04/schema#.

  • title — The name of the schema. Usually, this is a title for humans, but in WordPress, this field is created for reading by programs. Examples of route names for different data types: post, page, post type label, tag, taxonomy label, comment.

  • type — The type of data that will be received by the Client. Any of the seven primitive types can be specified here. In WordPress, this is almost always specified as object, even for collection endpoints that return an array of objects.

  • properties — A list of properties (parameters) that the object contains and the arguments of each property. Each property itself is also a schema, just without the top-level argument $schema. To differentiate, we can say that this is a sub-schema.

Example of creating such a schema from the WP core:

$schema = array(
	'$schema'    => 'http://json-schema.org/draft-04/schema#',
	'title'      => $this->post_type,
	'type'       => 'object',
	// Base properties for every Post.
	'properties' => array(
		'id'           => array(
			'description' => __( 'Unique identifier for the object.' ),
			'type'        => 'integer',
			'context'     => array( 'view', 'edit', 'embed' ),
			'readonly'    => true,
		),
		'date'         => array(
			'description' => __( "The date the object was published, in the site's timezone." ),
			'type'        => array( 'string', 'null' ),
			'format'      => 'date-time',
			'context'     => array( 'view', 'edit', 'embed' ),
		),
		'guid'         => array(
			'description' => __( 'The globally unique identifier for the object.' ),
			'type'        => 'object',
			'context'     => array( 'view', 'edit' ),
			'readonly'    => true,
			'properties'  => array(
				'raw'      => array(
					'description' => __( 'GUID for the object, as it exists in the database.' ),
					'type'        => 'string',
					'context'     => array( 'edit' ),
					'readonly'    => true,
				),
				'rendered' => array(
					'description' => __( 'GUID for the object, transformed for display.' ),
					'type'        => 'string',
					'context'     => array( 'view', 'edit' ),
					'readonly'    => true,
				),
			),
		),
		...
	),
);

Resource Schema

Resource Schema indicates what fields exist for the object (post, category, etc.). The resource schema can be specified when registering a route.

Let’s see how to create a comment resource schema:

// registering the route.
add_action( 'rest_api_init', 'kama_register_my_comment_route' );

function kama_register_my_comment_route() {

	register_rest_route( 'my-namespace/v1', '/comments', array(
		// registering
		array(
			'methods'  => 'GET',
			'callback' => 'kama_get_comments',
		),
		// registering the schema ("schema" is equated to the OPTIONS request)
		'schema' => 'kama_get_comment_schema',
	) );
}

# Gets the 5 latest comments and returns them as a REST response.
function kama_get_comments( $request ) {

	$data = array();

	$comments = get_comments( array(
		'post_per_page' => 5,
	) );

	// no comments, exit
	if ( empty( $comments ) )
		return rest_ensure_response( $data );

	foreach ( $comments as $comment ) {

		// add only those fields that are specified in the schema
		$schema = kama_get_comment_schema();

		$comment_data = array();

		// rename fields to more understandable ones
		if ( isset( $schema['properties']['id'] ) )
			$comment_data['id'] = (int) $comment->comment_id;

		if ( isset( $schema['properties']['author'] ) )
			$comment_data['author'] = (int) $comment->user_id;

		if ( isset( $schema['properties']['content'] ) )
			$comment_data['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment );

		$response = rest_ensure_response( $comment_data );

		$data[] = _kama_prepare_for_collection( $response );
	}

	// return all comment data.
	return rest_ensure_response( $data );
}

# Prepares the response for insertion into the collection of responses.
# Code copied from the WP_REST_Controller class.
function _kama_prepare_for_collection( $response ) {
	if ( ! ( $response instanceof WP_REST_Response ) )
		return $response;

	$data = (array) $response->get_data();
	$server = rest_get_server();

	if ( method_exists( $server, 'get_compact_response_links' ) )
		$links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
	else
		$links = call_user_func( array( $server, 'get_response_links' ), $response );

	if ( ! empty( $links ) )
		$data['_links'] = $links;

	return $data;
}

# Gets our schema for comments.
function kama_get_comment_schema() {
	$schema = array(
		// shows which version of the schema we are using - this is draft 4
		'$schema'              => 'http://json-schema.org/draft-04/schema#',
		// defines the resource described by the schema
		'title'                => 'comment',
		'type'                 => 'object',
		// in JSON schema, properties need to be specified in the 'properties' attribute.
		'properties'           => array(
			'id' => array(
				'description'  => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ),
				'type'         => 'integer',
				'context'      => array( 'view', 'edit', 'embed' ),
				'readonly'     => true,
			),
			'author' => array(
				'description'  => esc_html__( 'The id of the user object, if the author was a user.', 'my-textdomain' ),
				'type'         => 'integer',
			),
			'content' => array(
				'description'  => esc_html__( 'The content for the object.', 'my-textdomain' ),
				'type'         => 'string',
			),
		),
	);

	return $schema;
}

In lines 31-44, you can see that only the data fields of the comment that are specified in the schema we created are included in the response.

After creating the schema in this way, we can see it by making an OPTIONS request to the current route.

When we provided the Resource Schema, this resource becomes discoverable through the OPTIONS request to the current route.

Creating a Resource Schema is only one part of the overall Route Schema. The second part is creating the schema for endpoint parameters (see below).

Examples of resource schemas:

Endpoint Schemas and Their Parameters

Endpoint Schema describes the methods by which one can access the endpoint and its parameters.

When registering a route, its endpoints and the parameters of those endpoints (if parameters are needed at all) are always specified. For each parameter, you can specify its: description (description), type of value (type), whether the parameter is required (required). All this will be included in the Endpoint Schema.

Let’s consider an example of creating an endpoint with the parameter my_arg, for which we will specify the fields shown in the schema and functions for validating/sanitizing the value:

// registering the route.
add_action( 'rest_api_init', 'kama_register_my_route' );
function kama_register_my_route() {

	register_rest_route( 'my-namespace/v1', '/schema-arg', array(
		// registering the endpoint
		array(
			'methods'  => 'GET',
			'callback' => 'kama_get_item',
			// schema of arguments (parameters) of the endpoint. These parameters will appear in the route schema.
			'args'     => array(
				'arg_str' => array(
					'description'       => esc_html__('This is the argument our endpoint returns.','dom'),
					'type'              => 'string',
					'validate_callback' => 'kama_validate_params',
					'sanitize_callback' => 'kama_sanitize_params',
					'required'          => true,
				),
				'arg_int' => array(
					'description'       => esc_html__('This is the argument our endpoint returns.','dom'),
					'type'              => 'integer',
					'default'           => 10,
					'validate_callback' => 'kama_validate_params',
					'sanitize_callback' => 'kama_sanitize_params',
				),
				// etc.
			),
		),
	) );

}
## Returns parameters as a response to the request.
function kama_get_item( $request ) {
	// code will throw an error "arg_str not set" if the parameter is not passed in the request.
	// this is because we used required in the schema.
	return rest_ensure_response( $request['arg_str'] );
}

/**
 * Function to validate the parameter value.
 *
 * @param mixed           $value   The value of the parameter.
 * @param WP_REST_Request $request The current request object.
 * @param string          $param   The name of the parameter.
 */
function kama_validate_params( $value, $request, $param ) {

	$attributes = $request->get_attributes();

	$param_attr = & $attributes['args'][ $param ];

	// parameter passed from the schema
	if ( isset( $attributes['args'][ $param ] ) ) {
		// ensure that the parameter value is of the required type (string, integer)
		if (
			( 'string' === $param_attr['type'] && ! is_string( $value ) )
			||
			( 'integer' === $param_attr['type'] && ! is_numeric( $value ) )
		) {
			return new WP_Error( 'rest_invalid_param',
				sprintf( esc_html__('%s is not of type %s','dom'), $param, $param_attr ),
				array( 'status' => 400 )
			);
		}
	}
	// unknown parameter passed
	else {
		return new WP_Error( 'rest_invalid_param',
			sprintf( esc_html__('%s was not registered as a request argument.','dom'), $param ),
			array( 'status' => 400 )
		);
	}

	// if we reached here, the data has passed validation
	return true;
}

/**
 * Function to sanitize the parameter value.
 *
 * @param mixed           $value   The value of the parameter.
 * @param WP_REST_Request $request The current request object.
 * @param string          $param   The name of the parameter.
 */
function kama_sanitize_params( $value, $request, $param ) {

	$attributes = $request->get_attributes();

	// parameter passed from the schema
	if ( isset( $attributes['args'][ $param ] ) ) {
		// if the parameter value is a string, sanitize it as a string.
		if ( 'string' === $attributes['args'][ $param ]['type'] )
			return sanitize_text_field( $value );

		// if the parameter value is a number, sanitize it as a number.
		if ( 'integer' === $attributes['args'][ $param ]['type'] )
			return (int) $value;

	}
	// unknown parameter passed
	else {
		return new WP_Error( 'rest_invalid_param',
			sprintf( esc_html__('%s was not registered as a request argument.','dom'), $param ),
			array( 'status' => 400 )
		);
	}

	// if we reached here, then there is some error in this code. We should not reach this point.
	return new WP_Error( 'rest_api_sad',
		esc_html__('Something went terribly wrong.','dom'),
		array( 'status' => 500 )
	);

}

In the example above, we use validation and sanitization functions only for one request parameter my_arg. However, we can also use these validation and sanitization functions for any other parameter that is supposed to be a string (for which we specified the schema). As the code and endpoints grow, the schema will help keep the code easy and maintainable. Validating and sanitizing parameter values can be done without schemas, but in this case, it will be harder to track which validation/sanitization functions are used and where. Also, by adding a schema for request parameters, we show Clients our parameter schema. This helps Clients not to send invalid parameters in API requests.

Examples of parameter schemas