wp/wp-includes/rest-api.php
changeset 18 be944660c56a
parent 16 a86126ab1dd4
child 19 3d72ae0968f4
--- a/wp/wp-includes/rest-api.php	Tue Dec 15 15:52:01 2020 +0100
+++ b/wp/wp-includes/rest-api.php	Wed Sep 21 18:19:35 2022 +0200
@@ -94,7 +94,7 @@
 			_doing_it_wrong(
 				__FUNCTION__,
 				sprintf(
-					/* translators: 1. The REST API route being registered. 2. The argument name. 3. The suggested function name. */
+					/* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
 					__( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ),
 					'<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>',
 					'<code>permission_callback</code>',
@@ -130,7 +130,7 @@
  *     @type callable|null $update_callback Optional. The callback function used to set and update the field value. Default
  *                                          is 'null', the value cannot be set or updated. The function will be passed
  *                                          the model object, like WP_Post.
- *     @type array|null $schema             Optional. The callback function used to create the schema for this field.
+ *     @type array|null $schema             Optional. The schema for this field.
  *                                          Default is 'null', no schema entry will be returned.
  * }
  */
@@ -153,7 +153,7 @@
 }
 
 /**
- * Registers rewrite rules for the API.
+ * Registers rewrite rules for the REST API.
  *
  * @since 4.4.0
  *
@@ -209,6 +209,7 @@
 	add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
 
 	add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
+	add_filter( 'rest_index', 'rest_add_application_passwords_to_index' );
 }
 
 /**
@@ -264,10 +265,20 @@
 	$controller = new WP_REST_Users_Controller;
 	$controller->register_routes();
 
+	// Application Passwords
+	$controller = new WP_REST_Application_Passwords_Controller();
+	$controller->register_routes();
+
 	// Comments.
 	$controller = new WP_REST_Comments_Controller;
 	$controller->register_routes();
 
+	$search_handlers = array(
+		new WP_REST_Post_Search_Handler(),
+		new WP_REST_Term_Search_Handler(),
+		new WP_REST_Post_Format_Search_Handler(),
+	);
+
 	/**
 	 * Filters the search handlers to use in the REST search controller.
 	 *
@@ -277,7 +288,7 @@
 	 *                               handler instance must extend the `WP_REST_Search_Handler` class.
 	 *                               Default is only a handler for posts.
 	 */
-	$search_handlers = apply_filters( 'wp_rest_search_handlers', array( new WP_REST_Post_Search_Handler() ) );
+	$search_handlers = apply_filters( 'wp_rest_search_handlers', $search_handlers );
 
 	$controller = new WP_REST_Search_Controller( $search_handlers );
 	$controller->register_routes();
@@ -302,10 +313,30 @@
 	$controller = new WP_REST_Plugins_Controller();
 	$controller->register_routes();
 
+	// Sidebars.
+	$controller = new WP_REST_Sidebars_Controller();
+	$controller->register_routes();
+
+	// Widget Types.
+	$controller = new WP_REST_Widget_Types_Controller();
+	$controller->register_routes();
+
+	// Widgets.
+	$controller = new WP_REST_Widgets_Controller();
+	$controller->register_routes();
+
 	// Block Directory.
 	$controller = new WP_REST_Block_Directory_Controller();
 	$controller->register_routes();
 
+	// Pattern Directory.
+	$controller = new WP_REST_Pattern_Directory_Controller();
+	$controller->register_routes();
+
+	// Site Health.
+	$site_health = WP_Site_Health::get_instance();
+	$controller  = new WP_REST_Site_Health_Controller( $site_health );
+	$controller->register_routes();
 }
 
 /**
@@ -370,9 +401,9 @@
  * @todo Check if this is even necessary
  * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
  *
- * @param int    $blog_id Optional. Blog ID. Default of null returns URL for current blog.
- * @param string $path    Optional. REST route. Default '/'.
- * @param string $scheme  Optional. Sanitization scheme. Default 'rest'.
+ * @param int|null $blog_id Optional. Blog ID. Default of null returns URL for current blog.
+ * @param string   $path    Optional. REST route. Default '/'.
+ * @param string   $scheme  Optional. Sanitization scheme. Default 'rest'.
  * @return string Full URL to the endpoint.
  */
 function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
@@ -426,10 +457,10 @@
 	 *
 	 * @since 4.4.0
 	 *
-	 * @param string $url     REST URL.
-	 * @param string $path    REST route.
-	 * @param int    $blog_id Blog ID.
-	 * @param string $scheme  Sanitization scheme.
+	 * @param string   $url     REST URL.
+	 * @param string   $path    REST route.
+	 * @param int|null $blog_id Blog ID.
+	 * @param string   $scheme  Sanitization scheme.
 	 */
 	return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
 }
@@ -483,7 +514,7 @@
 		/**
 		 * Filters the REST Server Class.
 		 *
-		 * This filter allows you to adjust the server class used by the API, using a
+		 * This filter allows you to adjust the server class used by the REST API, using a
 		 * different class to handle requests.
 		 *
 		 * @since 4.4.0
@@ -494,7 +525,7 @@
 		$wp_rest_server       = new $wp_rest_server_class;
 
 		/**
-		 * Fires when preparing to serve an API request.
+		 * Fires when preparing to serve a REST API request.
 		 *
 		 * Endpoint objects should be created and register their hooks on this action rather
 		 * than another action to ensure they're only loaded when needed.
@@ -787,7 +818,7 @@
 }
 
 /**
- * Filter the API response to include only a white-listed set of response object fields.
+ * Filters the REST API response to include only a white-listed set of response object fields.
  *
  * @since 4.8.0
  *
@@ -997,7 +1028,7 @@
 	$result = wp_verify_nonce( $nonce, 'wp_rest' );
 
 	if ( ! $result ) {
-		return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
+		return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie check failed' ), array( 'status' => 403 ) );
 	}
 
 	// Send a refreshed nonce in header.
@@ -1030,6 +1061,104 @@
 }
 
 /**
+ * Collects the status of authenticating with an application password.
+ *
+ * @since 5.6.0
+ * @since 5.7.0 Added the `$app_password` parameter.
+ *
+ * @global WP_User|WP_Error|null $wp_rest_application_password_status
+ * @global string|null $wp_rest_application_password_uuid
+ *
+ * @param WP_Error $user_or_error The authenticated user or error instance.
+ * @param array    $app_password  The Application Password used to authenticate.
+ */
+function rest_application_password_collect_status( $user_or_error, $app_password = array() ) {
+	global $wp_rest_application_password_status, $wp_rest_application_password_uuid;
+
+	$wp_rest_application_password_status = $user_or_error;
+
+	if ( empty( $app_password['uuid'] ) ) {
+		$wp_rest_application_password_uuid = null;
+	} else {
+		$wp_rest_application_password_uuid = $app_password['uuid'];
+	}
+}
+
+/**
+ * Gets the Application Password used for authenticating the request.
+ *
+ * @since 5.7.0
+ *
+ * @global string|null $wp_rest_application_password_uuid
+ *
+ * @return string|null The App Password UUID, or null if Application Passwords was not used.
+ */
+function rest_get_authenticated_app_password() {
+	global $wp_rest_application_password_uuid;
+
+	return $wp_rest_application_password_uuid;
+}
+
+/**
+ * Checks for errors when using application password-based authentication.
+ *
+ * @since 5.6.0
+ *
+ * @global WP_User|WP_Error|null $wp_rest_application_password_status
+ *
+ * @param WP_Error|null|true $result Error from another authentication handler,
+ *                                   null if we should handle it, or another value if not.
+ * @return WP_Error|null|true WP_Error if the application password is invalid, the $result, otherwise true.
+ */
+function rest_application_password_check_errors( $result ) {
+	global $wp_rest_application_password_status;
+
+	if ( ! empty( $result ) ) {
+		return $result;
+	}
+
+	if ( is_wp_error( $wp_rest_application_password_status ) ) {
+		$data = $wp_rest_application_password_status->get_error_data();
+
+		if ( ! isset( $data['status'] ) ) {
+			$data['status'] = 401;
+		}
+
+		$wp_rest_application_password_status->add_data( $data );
+
+		return $wp_rest_application_password_status;
+	}
+
+	if ( $wp_rest_application_password_status instanceof WP_User ) {
+		return true;
+	}
+
+	return $result;
+}
+
+/**
+ * Adds Application Passwords info to the REST API index.
+ *
+ * @since 5.6.0
+ *
+ * @param WP_REST_Response $response The index response object.
+ * @return WP_REST_Response
+ */
+function rest_add_application_passwords_to_index( $response ) {
+	if ( ! wp_is_application_passwords_available() ) {
+		return $response;
+	}
+
+	$response->data['authentication']['application-passwords'] = array(
+		'endpoints' => array(
+			'authorization' => admin_url( 'authorize-application.php' ),
+		),
+	);
+
+	return $response;
+}
+
+/**
  * Retrieves the avatar urls in various sizes.
  *
  * @since 4.7.0
@@ -1161,7 +1290,7 @@
  *
  * @since 4.7.0
  *
- * @return integer 401 if the user is not logged in, 403 if the user is logged in.
+ * @return int 401 if the user is not logged in, 403 if the user is logged in.
  */
 function rest_authorization_required_code() {
 	return is_user_logged_in() ? 403 : 401;
@@ -1258,7 +1387,7 @@
  * @since 4.7.0
  *
  * @param bool|string|int $value The value being evaluated.
- * @return boolean Returns the proper associated boolean value.
+ * @return bool Returns the proper associated boolean value.
  */
 function rest_sanitize_boolean( $value ) {
 	// String values are translated to `true`; make sure 'false' is false.
@@ -1279,7 +1408,7 @@
  * @since 4.7.0
  *
  * @param bool|string $maybe_bool The value being evaluated.
- * @return boolean True if a boolean, otherwise false.
+ * @return bool True if a boolean, otherwise false.
  */
 function rest_is_boolean( $maybe_bool ) {
 	if ( is_bool( $maybe_bool ) ) {
@@ -1315,7 +1444,7 @@
  * @return bool True if an integer, otherwise false.
  */
 function rest_is_integer( $maybe_integer ) {
-	return is_numeric( $maybe_integer ) && round( floatval( $maybe_integer ) ) === floatval( $maybe_integer );
+	return is_numeric( $maybe_integer ) && round( (float) $maybe_integer ) === (float) $maybe_integer;
 }
 
 /**
@@ -1462,7 +1591,7 @@
 	if ( $invalid_types ) {
 		_doing_it_wrong(
 			__FUNCTION__,
-			/* translators: 1. Parameter. 2. List of allowed types. */
+			/* translators: 1: Parameter, 2: List of allowed types. */
 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only contain the built-in types: %2$l.' ), $param, $allowed_types ),
 			'5.5.0'
 		);
@@ -1540,6 +1669,362 @@
 }
 
 /**
+ * Validates if the JSON Schema pattern matches a value.
+ *
+ * @since 5.6.0
+ *
+ * @param string $pattern The pattern to match against.
+ * @param string $value   The value to check.
+ * @return bool           True if the pattern matches the given value, false otherwise.
+ */
+function rest_validate_json_schema_pattern( $pattern, $value ) {
+	$escaped_pattern = str_replace( '#', '\\#', $pattern );
+
+	return 1 === preg_match( '#' . $escaped_pattern . '#u', $value );
+}
+
+/**
+ * Finds the schema for a property using the patternProperties keyword.
+ *
+ * @since 5.6.0
+ *
+ * @param string $property The property name to check.
+ * @param array  $args     The schema array to use.
+ * @return array|null      The schema of matching pattern property, or null if no patterns match.
+ */
+function rest_find_matching_pattern_property_schema( $property, $args ) {
+	if ( isset( $args['patternProperties'] ) ) {
+		foreach ( $args['patternProperties'] as $pattern => $child_schema ) {
+			if ( rest_validate_json_schema_pattern( $pattern, $property ) ) {
+				return $child_schema;
+			}
+		}
+	}
+
+	return null;
+}
+
+/**
+ * Formats a combining operation error into a WP_Error object.
+ *
+ * @since 5.6.0
+ *
+ * @param string $param The parameter name.
+ * @param array $error  The error details.
+ * @return WP_Error
+ */
+function rest_format_combining_operation_error( $param, $error ) {
+	$position = $error['index'];
+	$reason   = $error['error_object']->get_error_message();
+
+	if ( isset( $error['schema']['title'] ) ) {
+		$title = $error['schema']['title'];
+
+		return new WP_Error(
+			'rest_no_matching_schema',
+			/* translators: 1: Parameter, 2: Schema title, 3: Reason. */
+			sprintf( __( '%1$s is not a valid %2$s. Reason: %3$s' ), $param, $title, $reason ),
+			array( 'position' => $position )
+		);
+	}
+
+	return new WP_Error(
+		'rest_no_matching_schema',
+		/* translators: 1: Parameter, 2: Reason. */
+		sprintf( __( '%1$s does not match the expected format. Reason: %2$s' ), $param, $reason ),
+		array( 'position' => $position )
+	);
+}
+
+/**
+ * Gets the error of combining operation.
+ *
+ * @since 5.6.0
+ *
+ * @param array  $value  The value to validate.
+ * @param string $param  The parameter name, used in error messages.
+ * @param array  $errors The errors array, to search for possible error.
+ * @return WP_Error      The combining operation error.
+ */
+function rest_get_combining_operation_error( $value, $param, $errors ) {
+	// If there is only one error, simply return it.
+	if ( 1 === count( $errors ) ) {
+		return rest_format_combining_operation_error( $param, $errors[0] );
+	}
+
+	// Filter out all errors related to type validation.
+	$filtered_errors = array();
+	foreach ( $errors as $error ) {
+		$error_code = $error['error_object']->get_error_code();
+		$error_data = $error['error_object']->get_error_data();
+
+		if ( 'rest_invalid_type' !== $error_code || ( isset( $error_data['param'] ) && $param !== $error_data['param'] ) ) {
+			$filtered_errors[] = $error;
+		}
+	}
+
+	// If there is only one error left, simply return it.
+	if ( 1 === count( $filtered_errors ) ) {
+		return rest_format_combining_operation_error( $param, $filtered_errors[0] );
+	}
+
+	// If there are only errors related to object validation, try choosing the most appropriate one.
+	if ( count( $filtered_errors ) > 1 && 'object' === $filtered_errors[0]['schema']['type'] ) {
+		$result = null;
+		$number = 0;
+
+		foreach ( $filtered_errors as $error ) {
+			if ( isset( $error['schema']['properties'] ) ) {
+				$n = count( array_intersect_key( $error['schema']['properties'], $value ) );
+				if ( $n > $number ) {
+					$result = $error;
+					$number = $n;
+				}
+			}
+		}
+
+		if ( null !== $result ) {
+			return rest_format_combining_operation_error( $param, $result );
+		}
+	}
+
+	// If each schema has a title, include those titles in the error message.
+	$schema_titles = array();
+	foreach ( $errors as $error ) {
+		if ( isset( $error['schema']['title'] ) ) {
+			$schema_titles[] = $error['schema']['title'];
+		}
+	}
+
+	if ( count( $schema_titles ) === count( $errors ) ) {
+		/* translators: 1: Parameter, 2: Schema titles. */
+		return new WP_Error( 'rest_no_matching_schema', wp_sprintf( __( '%1$s is not a valid %2$l.' ), $param, $schema_titles ) );
+	}
+
+	/* translators: %s: Parameter. */
+	return new WP_Error( 'rest_no_matching_schema', sprintf( __( '%s does not match any of the expected formats.' ), $param ) );
+}
+
+/**
+ * Finds the matching schema among the "anyOf" schemas.
+ *
+ * @since 5.6.0
+ *
+ * @param mixed  $value   The value to validate.
+ * @param array  $args    The schema array to use.
+ * @param string $param   The parameter name, used in error messages.
+ * @return array|WP_Error The matching schema or WP_Error instance if all schemas do not match.
+ */
+function rest_find_any_matching_schema( $value, $args, $param ) {
+	$errors = array();
+
+	foreach ( $args['anyOf'] as $index => $schema ) {
+		if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
+			$schema['type'] = $args['type'];
+		}
+
+		$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
+		if ( ! is_wp_error( $is_valid ) ) {
+			return $schema;
+		}
+
+		$errors[] = array(
+			'error_object' => $is_valid,
+			'schema'       => $schema,
+			'index'        => $index,
+		);
+	}
+
+	return rest_get_combining_operation_error( $value, $param, $errors );
+}
+
+/**
+ * Finds the matching schema among the "oneOf" schemas.
+ *
+ * @since 5.6.0
+ *
+ * @param mixed  $value                  The value to validate.
+ * @param array  $args                   The schema array to use.
+ * @param string $param                  The parameter name, used in error messages.
+ * @param bool   $stop_after_first_match Optional. Whether the process should stop after the first successful match.
+ * @return array|WP_Error                The matching schema or WP_Error instance if the number of matching schemas is not equal to one.
+ */
+function rest_find_one_matching_schema( $value, $args, $param, $stop_after_first_match = false ) {
+	$matching_schemas = array();
+	$errors           = array();
+
+	foreach ( $args['oneOf'] as $index => $schema ) {
+		if ( ! isset( $schema['type'] ) && isset( $args['type'] ) ) {
+			$schema['type'] = $args['type'];
+		}
+
+		$is_valid = rest_validate_value_from_schema( $value, $schema, $param );
+		if ( ! is_wp_error( $is_valid ) ) {
+			if ( $stop_after_first_match ) {
+				return $schema;
+			}
+
+			$matching_schemas[] = array(
+				'schema_object' => $schema,
+				'index'         => $index,
+			);
+		} else {
+			$errors[] = array(
+				'error_object' => $is_valid,
+				'schema'       => $schema,
+				'index'        => $index,
+			);
+		}
+	}
+
+	if ( ! $matching_schemas ) {
+		return rest_get_combining_operation_error( $value, $param, $errors );
+	}
+
+	if ( count( $matching_schemas ) > 1 ) {
+		$schema_positions = array();
+		$schema_titles    = array();
+
+		foreach ( $matching_schemas as $schema ) {
+			$schema_positions[] = $schema['index'];
+
+			if ( isset( $schema['schema_object']['title'] ) ) {
+				$schema_titles[] = $schema['schema_object']['title'];
+			}
+		}
+
+		// If each schema has a title, include those titles in the error message.
+		if ( count( $schema_titles ) === count( $matching_schemas ) ) {
+			return new WP_Error(
+				'rest_one_of_multiple_matches',
+				/* translators: 1: Parameter, 2: Schema titles. */
+				wp_sprintf( __( '%1$s matches %2$l, but should match only one.' ), $param, $schema_titles ),
+				array( 'positions' => $schema_positions )
+			);
+		}
+
+		return new WP_Error(
+			'rest_one_of_multiple_matches',
+			/* translators: %s: Parameter. */
+			sprintf( __( '%s matches more than one of the expected formats.' ), $param ),
+			array( 'positions' => $schema_positions )
+		);
+	}
+
+	return $matching_schemas[0]['schema_object'];
+}
+
+/**
+ * Checks the equality of two values, following JSON Schema semantics.
+ *
+ * Property order is ignored for objects.
+ *
+ * Values must have been previously sanitized/coerced to their native types.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed $value1 The first value to check.
+ * @param mixed $value2 The second value to check.
+ * @return bool True if the values are equal or false otherwise.
+ */
+function rest_are_values_equal( $value1, $value2 ) {
+	if ( is_array( $value1 ) && is_array( $value2 ) ) {
+		if ( count( $value1 ) !== count( $value2 ) ) {
+			return false;
+		}
+
+		foreach ( $value1 as $index => $value ) {
+			if ( ! array_key_exists( $index, $value2 ) || ! rest_are_values_equal( $value, $value2[ $index ] ) ) {
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	if ( is_int( $value1 ) && is_float( $value2 )
+		|| is_float( $value1 ) && is_int( $value2 )
+	) {
+		return (float) $value1 === (float) $value2;
+	}
+
+	return $value1 === $value2;
+}
+
+/**
+ * Validates that the given value is a member of the JSON Schema "enum".
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value  The value to validate.
+ * @param array  $args   The schema array to use.
+ * @param string $param  The parameter name, used in error messages.
+ * @return true|WP_Error True if the "enum" contains the value or a WP_Error instance otherwise.
+ */
+function rest_validate_enum( $value, $args, $param ) {
+	$sanitized_value = rest_sanitize_value_from_schema( $value, $args, $param );
+	if ( is_wp_error( $sanitized_value ) ) {
+		return $sanitized_value;
+	}
+
+	foreach ( $args['enum'] as $enum_value ) {
+		if ( rest_are_values_equal( $sanitized_value, $enum_value ) ) {
+			return true;
+		}
+	}
+
+	$encoded_enum_values = array();
+	foreach ( $args['enum'] as $enum_value ) {
+		$encoded_enum_values[] = is_scalar( $enum_value ) ? $enum_value : wp_json_encode( $enum_value );
+	}
+
+	if ( count( $encoded_enum_values ) === 1 ) {
+		/* translators: 1: Parameter, 2: Valid values. */
+		return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not %2$s.' ), $param, $encoded_enum_values[0] ) );
+	}
+
+	/* translators: 1: Parameter, 2: List of valid values. */
+	return new WP_Error( 'rest_not_in_enum', wp_sprintf( __( '%1$s is not one of %2$l.' ), $param, $encoded_enum_values ) );
+}
+
+/**
+ * Get all valid JSON schema properties.
+ *
+ * @since 5.6.0
+ *
+ * @return string[] All valid JSON schema properties.
+ */
+function rest_get_allowed_schema_keywords() {
+	return array(
+		'title',
+		'description',
+		'default',
+		'type',
+		'format',
+		'enum',
+		'items',
+		'properties',
+		'additionalProperties',
+		'patternProperties',
+		'minProperties',
+		'maxProperties',
+		'minimum',
+		'maximum',
+		'exclusiveMinimum',
+		'exclusiveMaximum',
+		'multipleOf',
+		'minLength',
+		'maxLength',
+		'pattern',
+		'minItems',
+		'maxItems',
+		'uniqueItems',
+		'anyOf',
+		'oneOf',
+	);
+}
+
+/**
  * Validate a value based on a schema.
  *
  * @since 4.7.0
@@ -1551,6 +2036,10 @@
  *              Support the "minLength", "maxLength" and "pattern" keywords for strings.
  *              Support the "minItems", "maxItems" and "uniqueItems" keywords for arrays.
  *              Validate required properties.
+ * @since 5.6.0 Support the "minProperties" and "maxProperties" keywords for objects.
+ *              Support the "multipleOf" keyword for numbers and integers.
+ *              Support the "patternProperties" keyword for objects.
+ *              Support the "anyOf" and "oneOf" keywords.
  *
  * @param mixed  $value The value to validate.
  * @param array  $args  Schema array to use for validation.
@@ -1558,10 +2047,32 @@
  * @return true|WP_Error
  */
 function rest_validate_value_from_schema( $value, $args, $param = '' ) {
+	if ( isset( $args['anyOf'] ) ) {
+		$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
+		if ( is_wp_error( $matching_schema ) ) {
+			return $matching_schema;
+		}
+
+		if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
+			$args['type'] = $matching_schema['type'];
+		}
+	}
+
+	if ( isset( $args['oneOf'] ) ) {
+		$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
+		if ( is_wp_error( $matching_schema ) ) {
+			return $matching_schema;
+		}
+
+		if ( ! isset( $args['type'] ) && isset( $matching_schema['type'] ) ) {
+			$args['type'] = $matching_schema['type'];
+		}
+	}
+
 	$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
 
 	if ( ! isset( $args['type'] ) ) {
-		/* translators: 1. Parameter */
+		/* translators: %s: Parameter. */
 		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
 	}
 
@@ -1569,8 +2080,12 @@
 		$best_type = rest_handle_multi_type_schema( $value, $args, $param );
 
 		if ( ! $best_type ) {
-			/* translators: 1: Parameter, 2: List of types. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ) );
+			return new WP_Error(
+				'rest_invalid_type',
+				/* translators: 1: Parameter, 2: List of types. */
+				sprintf( __( '%1$s is not of type %2$s.' ), $param, implode( ',', $args['type'] ) ),
+				array( 'param' => $param )
+			);
 		}
 
 		$args['type'] = $best_type;
@@ -1579,158 +2094,47 @@
 	if ( ! in_array( $args['type'], $allowed_types, true ) ) {
 		_doing_it_wrong(
 			__FUNCTION__,
-			/* translators: 1. Parameter 2. The list of allowed types. */
+			/* translators: 1: Parameter, 2: The list of allowed types. */
 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
 			'5.5.0'
 		);
 	}
 
-	if ( 'array' === $args['type'] ) {
-		if ( ! rest_is_array( $value ) ) {
-			/* translators: 1: Parameter, 2: Type name. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ) );
-		}
-
-		$value = rest_sanitize_array( $value );
-
-		if ( isset( $args['items'] ) ) {
-			foreach ( $value as $index => $v ) {
-				$is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
-				if ( is_wp_error( $is_valid ) ) {
-					return $is_valid;
-				}
-			}
-		}
-
-		if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) {
-			/* translators: 1: Parameter, 2: Number. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at least %2$s items.' ), $param, number_format_i18n( $args['minItems'] ) ) );
-		}
-
-		if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) {
-			/* translators: 1: Parameter, 2: Number. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must contain at most %2$s items.' ), $param, number_format_i18n( $args['maxItems'] ) ) );
-		}
-
-		if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
-			/* translators: 1: Parameter */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
-		}
+	switch ( $args['type'] ) {
+		case 'null':
+			$is_valid = rest_validate_null_value_from_schema( $value, $param );
+			break;
+		case 'boolean':
+			$is_valid = rest_validate_boolean_value_from_schema( $value, $param );
+			break;
+		case 'object':
+			$is_valid = rest_validate_object_value_from_schema( $value, $args, $param );
+			break;
+		case 'array':
+			$is_valid = rest_validate_array_value_from_schema( $value, $args, $param );
+			break;
+		case 'number':
+			$is_valid = rest_validate_number_value_from_schema( $value, $args, $param );
+			break;
+		case 'string':
+			$is_valid = rest_validate_string_value_from_schema( $value, $args, $param );
+			break;
+		case 'integer':
+			$is_valid = rest_validate_integer_value_from_schema( $value, $args, $param );
+			break;
+		default:
+			$is_valid = true;
+			break;
 	}
 
-	if ( 'object' === $args['type'] ) {
-		if ( ! rest_is_object( $value ) ) {
-			/* translators: 1: Parameter, 2: Type name. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ) );
-		}
-
-		$value = rest_sanitize_object( $value );
-
-		if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
-			foreach ( $args['required'] as $name ) {
-				if ( ! array_key_exists( $name, $value ) ) {
-					/* translators: 1: Property of an object, 2: Parameter. */
-					return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) );
-				}
-			}
-		} elseif ( isset( $args['properties'] ) ) { // schema version 3
-			foreach ( $args['properties'] as $name => $property ) {
-				if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) {
-					/* translators: 1: Property of an object, 2: Parameter. */
-					return new WP_Error( 'rest_property_required', sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param ) );
-				}
-			}
-		}
-
-		foreach ( $value as $property => $v ) {
-			if ( isset( $args['properties'][ $property ] ) ) {
-				$is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
-				if ( is_wp_error( $is_valid ) ) {
-					return $is_valid;
-				}
-			} elseif ( isset( $args['additionalProperties'] ) ) {
-				if ( false === $args['additionalProperties'] ) {
-					/* translators: %s: Property of an object. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not a valid property of Object.' ), $property ) );
-				}
-
-				if ( is_array( $args['additionalProperties'] ) ) {
-					$is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
-					if ( is_wp_error( $is_valid ) ) {
-						return $is_valid;
-					}
-				}
-			}
-		}
-	}
-
-	if ( 'null' === $args['type'] ) {
-		if ( null !== $value ) {
-			/* translators: 1: Parameter, 2: Type name. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ) );
-		}
-
-		return true;
+	if ( is_wp_error( $is_valid ) ) {
+		return $is_valid;
 	}
 
 	if ( ! empty( $args['enum'] ) ) {
-		if ( ! in_array( $value, $args['enum'], true ) ) {
-			/* translators: 1: Parameter, 2: List of valid values. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not one of %2$s.' ), $param, implode( ', ', $args['enum'] ) ) );
-		}
-	}
-
-	if ( in_array( $args['type'], array( 'integer', 'number' ), true ) && ! is_numeric( $value ) ) {
-		/* translators: 1: Parameter, 2: Type name. */
-		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ) );
-	}
-
-	if ( 'integer' === $args['type'] && ! rest_is_integer( $value ) ) {
-		/* translators: 1: Parameter, 2: Type name. */
-		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ) );
-	}
-
-	if ( 'boolean' === $args['type'] && ! rest_is_boolean( $value ) ) {
-		/* translators: 1: Parameter, 2: Type name. */
-		return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ) );
-	}
-
-	if ( 'string' === $args['type'] ) {
-		if ( ! is_string( $value ) ) {
-			/* translators: 1: Parameter, 2: Type name. */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ) );
-		}
-
-		if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
-			return new WP_Error(
-				'rest_invalid_param',
-				sprintf(
-					/* translators: 1: Parameter, 2: Number of characters. */
-					_n( '%1$s must be at least %2$s character long.', '%1$s must be at least %2$s characters long.', $args['minLength'] ),
-					$param,
-					number_format_i18n( $args['minLength'] )
-				)
-			);
-		}
-
-		if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) {
-			return new WP_Error(
-				'rest_invalid_param',
-				sprintf(
-					/* translators: 1: Parameter, 2: Number of characters. */
-					_n( '%1$s must be at most %2$s character long.', '%1$s must be at most %2$s characters long.', $args['maxLength'] ),
-					$param,
-					number_format_i18n( $args['maxLength'] )
-				)
-			);
-		}
-
-		if ( isset( $args['pattern'] ) ) {
-			$pattern = str_replace( '#', '\\#', $args['pattern'] );
-			if ( ! preg_match( '#' . $pattern . '#u', $value ) ) {
-				/* translators: 1: Parameter, 2: Pattern. */
-				return new WP_Error( 'rest_invalid_pattern', sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] ) );
-			}
+		$enum_contains_value = rest_validate_enum( $value, $args, $param );
+		if ( is_wp_error( $enum_contains_value ) ) {
+			return $enum_contains_value;
 		}
 	}
 
@@ -1760,60 +2164,468 @@
 			case 'ip':
 				if ( ! rest_is_ip_address( $value ) ) {
 					/* translators: %s: IP address. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%s is not a valid IP address.' ), $param ) );
+					return new WP_Error( 'rest_invalid_ip', sprintf( __( '%s is not a valid IP address.' ), $param ) );
 				}
 				break;
 			case 'uuid':
 				if ( ! wp_is_uuid( $value ) ) {
-					/* translators: %s is the name of a JSON field expecting a valid uuid. */
+					/* translators: %s: The name of a JSON field expecting a valid UUID. */
 					return new WP_Error( 'rest_invalid_uuid', sprintf( __( '%s is not a valid UUID.' ), $param ) );
 				}
 				break;
 		}
 	}
 
-	if ( in_array( $args['type'], array( 'number', 'integer' ), true ) && ( isset( $args['minimum'] ) || isset( $args['maximum'] ) ) ) {
-		if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
-			if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
-				/* translators: 1: Parameter, 2: Minimum number. */
-				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] ) );
-			} elseif ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
-				/* translators: 1: Parameter, 2: Minimum number. */
-				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] ) );
-			}
-		} elseif ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
-			if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
-				/* translators: 1: Parameter, 2: Maximum number. */
-				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] ) );
-			} elseif ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
-				/* translators: 1: Parameter, 2: Maximum number. */
-				return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] ) );
+	return true;
+}
+
+/**
+ * Validates a null value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_null_value_from_schema( $value, $param ) {
+	if ( null !== $value ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'null' ),
+			array( 'param' => $param )
+		);
+	}
+
+	return true;
+}
+
+/**
+ * Validates a boolean value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_boolean_value_from_schema( $value, $param ) {
+	if ( ! rest_is_boolean( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'boolean' ),
+			array( 'param' => $param )
+		);
+	}
+
+	return true;
+}
+
+/**
+ * Validates an object value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param array  $args  Schema array to use for validation.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_object_value_from_schema( $value, $args, $param ) {
+	if ( ! rest_is_object( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'object' ),
+			array( 'param' => $param )
+		);
+	}
+
+	$value = rest_sanitize_object( $value );
+
+	if ( isset( $args['required'] ) && is_array( $args['required'] ) ) { // schema version 4
+		foreach ( $args['required'] as $name ) {
+			if ( ! array_key_exists( $name, $value ) ) {
+				return new WP_Error(
+					'rest_property_required',
+					/* translators: 1: Property of an object, 2: Parameter. */
+					sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
+				);
 			}
-		} elseif ( isset( $args['maximum'] ) && isset( $args['minimum'] ) ) {
-			if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
-				if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
-					/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
-				}
-			} elseif ( empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
-				if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
-					/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
-				}
-			} elseif ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
-				if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
-					/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
-				}
-			} elseif ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
-				if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
-					/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
-					return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ), $param, $args['minimum'], $args['maximum'] ) );
+		}
+	} elseif ( isset( $args['properties'] ) ) { // schema version 3
+		foreach ( $args['properties'] as $name => $property ) {
+			if ( isset( $property['required'] ) && true === $property['required'] && ! array_key_exists( $name, $value ) ) {
+				return new WP_Error(
+					'rest_property_required',
+					/* translators: 1: Property of an object, 2: Parameter. */
+					sprintf( __( '%1$s is a required property of %2$s.' ), $name, $param )
+				);
+			}
+		}
+	}
+
+	foreach ( $value as $property => $v ) {
+		if ( isset( $args['properties'][ $property ] ) ) {
+			$is_valid = rest_validate_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
+			if ( is_wp_error( $is_valid ) ) {
+				return $is_valid;
+			}
+			continue;
+		}
+
+		$pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
+		if ( null !== $pattern_property_schema ) {
+			$is_valid = rest_validate_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
+			if ( is_wp_error( $is_valid ) ) {
+				return $is_valid;
+			}
+			continue;
+		}
+
+		if ( isset( $args['additionalProperties'] ) ) {
+			if ( false === $args['additionalProperties'] ) {
+				return new WP_Error(
+					'rest_additional_properties_forbidden',
+					/* translators: %s: Property of an object. */
+					sprintf( __( '%1$s is not a valid property of Object.' ), $property )
+				);
+			}
+
+			if ( is_array( $args['additionalProperties'] ) ) {
+				$is_valid = rest_validate_value_from_schema( $v, $args['additionalProperties'], $param . '[' . $property . ']' );
+				if ( is_wp_error( $is_valid ) ) {
+					return $is_valid;
 				}
 			}
 		}
 	}
 
+	if ( isset( $args['minProperties'] ) && count( $value ) < $args['minProperties'] ) {
+		return new WP_Error(
+			'rest_too_few_properties',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number. */
+				_n(
+					'%1$s must contain at least %2$s property.',
+					'%1$s must contain at least %2$s properties.',
+					$args['minProperties']
+				),
+				$param,
+				number_format_i18n( $args['minProperties'] )
+			)
+		);
+	}
+
+	if ( isset( $args['maxProperties'] ) && count( $value ) > $args['maxProperties'] ) {
+		return new WP_Error(
+			'rest_too_many_properties',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number. */
+				_n(
+					'%1$s must contain at most %2$s property.',
+					'%1$s must contain at most %2$s properties.',
+					$args['maxProperties']
+				),
+				$param,
+				number_format_i18n( $args['maxProperties'] )
+			)
+		);
+	}
+
+	return true;
+}
+
+/**
+ * Validates an array value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param array  $args  Schema array to use for validation.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_array_value_from_schema( $value, $args, $param ) {
+	if ( ! rest_is_array( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'array' ),
+			array( 'param' => $param )
+		);
+	}
+
+	$value = rest_sanitize_array( $value );
+
+	if ( isset( $args['items'] ) ) {
+		foreach ( $value as $index => $v ) {
+			$is_valid = rest_validate_value_from_schema( $v, $args['items'], $param . '[' . $index . ']' );
+			if ( is_wp_error( $is_valid ) ) {
+				return $is_valid;
+			}
+		}
+	}
+
+	if ( isset( $args['minItems'] ) && count( $value ) < $args['minItems'] ) {
+		return new WP_Error(
+			'rest_too_few_items',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number. */
+				_n(
+					'%1$s must contain at least %2$s item.',
+					'%1$s must contain at least %2$s items.',
+					$args['minItems']
+				),
+				$param,
+				number_format_i18n( $args['minItems'] )
+			)
+		);
+	}
+
+	if ( isset( $args['maxItems'] ) && count( $value ) > $args['maxItems'] ) {
+		return new WP_Error(
+			'rest_too_many_items',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number. */
+				_n(
+					'%1$s must contain at most %2$s item.',
+					'%1$s must contain at most %2$s items.',
+					$args['maxItems']
+				),
+				$param,
+				number_format_i18n( $args['maxItems'] )
+			)
+		);
+	}
+
+	if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
+		/* translators: %s: Parameter. */
+		return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
+	}
+
+	return true;
+}
+
+/**
+ * Validates a number value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param array  $args  Schema array to use for validation.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_number_value_from_schema( $value, $args, $param ) {
+	if ( ! is_numeric( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, $args['type'] ),
+			array( 'param' => $param )
+		);
+	}
+
+	if ( isset( $args['multipleOf'] ) && fmod( $value, $args['multipleOf'] ) !== 0.0 ) {
+		return new WP_Error(
+			'rest_invalid_multiple',
+			/* translators: 1: Parameter, 2: Multiplier. */
+			sprintf( __( '%1$s must be a multiple of %2$s.' ), $param, $args['multipleOf'] )
+		);
+	}
+
+	if ( isset( $args['minimum'] ) && ! isset( $args['maximum'] ) ) {
+		if ( ! empty( $args['exclusiveMinimum'] ) && $value <= $args['minimum'] ) {
+			return new WP_Error(
+				'rest_out_of_bounds',
+				/* translators: 1: Parameter, 2: Minimum number. */
+				sprintf( __( '%1$s must be greater than %2$d' ), $param, $args['minimum'] )
+			);
+		}
+
+		if ( empty( $args['exclusiveMinimum'] ) && $value < $args['minimum'] ) {
+			return new WP_Error(
+				'rest_out_of_bounds',
+				/* translators: 1: Parameter, 2: Minimum number. */
+				sprintf( __( '%1$s must be greater than or equal to %2$d' ), $param, $args['minimum'] )
+			);
+		}
+	}
+
+	if ( isset( $args['maximum'] ) && ! isset( $args['minimum'] ) ) {
+		if ( ! empty( $args['exclusiveMaximum'] ) && $value >= $args['maximum'] ) {
+			return new WP_Error(
+				'rest_out_of_bounds',
+				/* translators: 1: Parameter, 2: Maximum number. */
+				sprintf( __( '%1$s must be less than %2$d' ), $param, $args['maximum'] )
+			);
+		}
+
+		if ( empty( $args['exclusiveMaximum'] ) && $value > $args['maximum'] ) {
+			return new WP_Error(
+				'rest_out_of_bounds',
+				/* translators: 1: Parameter, 2: Maximum number. */
+				sprintf( __( '%1$s must be less than or equal to %2$d' ), $param, $args['maximum'] )
+			);
+		}
+	}
+
+	if ( isset( $args['minimum'], $args['maximum'] ) ) {
+		if ( ! empty( $args['exclusiveMinimum'] ) && ! empty( $args['exclusiveMaximum'] ) ) {
+			if ( $value >= $args['maximum'] || $value <= $args['minimum'] ) {
+				return new WP_Error(
+					'rest_out_of_bounds',
+					sprintf(
+						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
+						__( '%1$s must be between %2$d (exclusive) and %3$d (exclusive)' ),
+						$param,
+						$args['minimum'],
+						$args['maximum']
+					)
+				);
+			}
+		}
+
+		if ( ! empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
+			if ( $value > $args['maximum'] || $value <= $args['minimum'] ) {
+				return new WP_Error(
+					'rest_out_of_bounds',
+					sprintf(
+						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
+						__( '%1$s must be between %2$d (exclusive) and %3$d (inclusive)' ),
+						$param,
+						$args['minimum'],
+						$args['maximum']
+					)
+				);
+			}
+		}
+
+		if ( ! empty( $args['exclusiveMaximum'] ) && empty( $args['exclusiveMinimum'] ) ) {
+			if ( $value >= $args['maximum'] || $value < $args['minimum'] ) {
+				return new WP_Error(
+					'rest_out_of_bounds',
+					sprintf(
+						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
+						__( '%1$s must be between %2$d (inclusive) and %3$d (exclusive)' ),
+						$param,
+						$args['minimum'],
+						$args['maximum']
+					)
+				);
+			}
+		}
+
+		if ( empty( $args['exclusiveMinimum'] ) && empty( $args['exclusiveMaximum'] ) ) {
+			if ( $value > $args['maximum'] || $value < $args['minimum'] ) {
+				return new WP_Error(
+					'rest_out_of_bounds',
+					sprintf(
+						/* translators: 1: Parameter, 2: Minimum number, 3: Maximum number. */
+						__( '%1$s must be between %2$d (inclusive) and %3$d (inclusive)' ),
+						$param,
+						$args['minimum'],
+						$args['maximum']
+					)
+				);
+			}
+		}
+	}
+
+	return true;
+}
+
+/**
+ * Validates a string value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param array  $args  Schema array to use for validation.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_string_value_from_schema( $value, $args, $param ) {
+	if ( ! is_string( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'string' ),
+			array( 'param' => $param )
+		);
+	}
+
+	if ( isset( $args['minLength'] ) && mb_strlen( $value ) < $args['minLength'] ) {
+		return new WP_Error(
+			'rest_too_short',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number of characters. */
+				_n(
+					'%1$s must be at least %2$s character long.',
+					'%1$s must be at least %2$s characters long.',
+					$args['minLength']
+				),
+				$param,
+				number_format_i18n( $args['minLength'] )
+			)
+		);
+	}
+
+	if ( isset( $args['maxLength'] ) && mb_strlen( $value ) > $args['maxLength'] ) {
+		return new WP_Error(
+			'rest_too_long',
+			sprintf(
+				/* translators: 1: Parameter, 2: Number of characters. */
+				_n(
+					'%1$s must be at most %2$s character long.',
+					'%1$s must be at most %2$s characters long.',
+					$args['maxLength']
+				),
+				$param,
+				number_format_i18n( $args['maxLength'] )
+			)
+		);
+	}
+
+	if ( isset( $args['pattern'] ) && ! rest_validate_json_schema_pattern( $args['pattern'], $value ) ) {
+		return new WP_Error(
+			'rest_invalid_pattern',
+			/* translators: 1: Parameter, 2: Pattern. */
+			sprintf( __( '%1$s does not match pattern %2$s.' ), $param, $args['pattern'] )
+		);
+	}
+
+	return true;
+}
+
+/**
+ * Validates an integer value based on a schema.
+ *
+ * @since 5.7.0
+ *
+ * @param mixed  $value The value to validate.
+ * @param array  $args  Schema array to use for validation.
+ * @param string $param The parameter name, used in error messages.
+ * @return true|WP_Error
+ */
+function rest_validate_integer_value_from_schema( $value, $args, $param ) {
+	$is_valid_number = rest_validate_number_value_from_schema( $value, $args, $param );
+	if ( is_wp_error( $is_valid_number ) ) {
+		return $is_valid_number;
+	}
+
+	if ( ! rest_is_integer( $value ) ) {
+		return new WP_Error(
+			'rest_invalid_type',
+			/* translators: 1: Parameter, 2: Type name. */
+			sprintf( __( '%1$s is not of type %2$s.' ), $param, 'integer' ),
+			array( 'param' => $param )
+		);
+	}
+
 	return true;
 }
 
@@ -1822,6 +2634,7 @@
  *
  * @since 4.7.0
  * @since 5.5.0 Added the `$param` parameter.
+ * @since 5.6.0 Support the "anyOf" and "oneOf" keywords.
  *
  * @param mixed  $value The value to sanitize.
  * @param array  $args  Schema array to use for sanitization.
@@ -1829,10 +2642,36 @@
  * @return mixed|WP_Error The sanitized value or a WP_Error instance if the value cannot be safely sanitized.
  */
 function rest_sanitize_value_from_schema( $value, $args, $param = '' ) {
+	if ( isset( $args['anyOf'] ) ) {
+		$matching_schema = rest_find_any_matching_schema( $value, $args, $param );
+		if ( is_wp_error( $matching_schema ) ) {
+			return $matching_schema;
+		}
+
+		if ( ! isset( $args['type'] ) ) {
+			$args['type'] = $matching_schema['type'];
+		}
+
+		$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
+	}
+
+	if ( isset( $args['oneOf'] ) ) {
+		$matching_schema = rest_find_one_matching_schema( $value, $args, $param );
+		if ( is_wp_error( $matching_schema ) ) {
+			return $matching_schema;
+		}
+
+		if ( ! isset( $args['type'] ) ) {
+			$args['type'] = $matching_schema['type'];
+		}
+
+		$value = rest_sanitize_value_from_schema( $value, $matching_schema, $param );
+	}
+
 	$allowed_types = array( 'array', 'object', 'string', 'number', 'integer', 'boolean', 'null' );
 
 	if ( ! isset( $args['type'] ) ) {
-		/* translators: 1. Parameter */
+		/* translators: %s: Parameter. */
 		_doing_it_wrong( __FUNCTION__, sprintf( __( 'The "type" schema keyword for %s is required.' ), $param ), '5.5.0' );
 	}
 
@@ -1849,7 +2688,7 @@
 	if ( ! in_array( $args['type'], $allowed_types, true ) ) {
 		_doing_it_wrong(
 			__FUNCTION__,
-			/* translators: 1. Parameter. 2. The list of allowed types. */
+			/* translators: 1: Parameter, 2: The list of allowed types. */
 			wp_sprintf( __( 'The "type" schema keyword for %1$s can only be one of the built-in types: %2$l.' ), $param, $allowed_types ),
 			'5.5.0'
 		);
@@ -1865,8 +2704,8 @@
 		}
 
 		if ( ! empty( $args['uniqueItems'] ) && ! rest_validate_array_contains_unique_items( $value ) ) {
-			/* translators: 1: Parameter */
-			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s has duplicate items.' ), $param ) );
+			/* translators: %s: Parameter. */
+			return new WP_Error( 'rest_duplicate_items', sprintf( __( '%s has duplicate items.' ), $param ) );
 		}
 
 		return $value;
@@ -1878,7 +2717,16 @@
 		foreach ( $value as $property => $v ) {
 			if ( isset( $args['properties'][ $property ] ) ) {
 				$value[ $property ] = rest_sanitize_value_from_schema( $v, $args['properties'][ $property ], $param . '[' . $property . ']' );
-			} elseif ( isset( $args['additionalProperties'] ) ) {
+				continue;
+			}
+
+			$pattern_property_schema = rest_find_matching_pattern_property_schema( $property, $args );
+			if ( null !== $pattern_property_schema ) {
+				$value[ $property ] = rest_sanitize_value_from_schema( $v, $pattern_property_schema, $param . '[' . $property . ']' );
+				continue;
+			}
+
+			if ( isset( $args['additionalProperties'] ) ) {
 				if ( false === $args['additionalProperties'] ) {
 					unset( $value[ $property ] );
 				} elseif ( is_array( $args['additionalProperties'] ) ) {
@@ -1933,7 +2781,7 @@
 	}
 
 	if ( 'string' === $args['type'] ) {
-		return strval( $value );
+		return (string) $value;
 	}
 
 	return $value;
@@ -1984,11 +2832,8 @@
 	$response = rest_do_request( $request );
 	if ( 200 === $response->status ) {
 		$server = rest_get_server();
-		$data   = (array) $response->get_data();
-		$links  = $server::get_compact_response_links( $response );
-		if ( ! empty( $links ) ) {
-			$data['_links'] = $links;
-		}
+		$embed  = $request->has_param( '_embed' ) ? rest_parse_embed_param( $request['_embed'] ) : false;
+		$data   = (array) $server->response_to_data( $response, $embed );
 
 		if ( 'OPTIONS' === $method ) {
 			$response = rest_send_allow_header( $response, $server, $request );
@@ -2034,6 +2879,8 @@
  * Filters the response to remove any fields not available in the given context.
  *
  * @since 5.5.0
+ * @since 5.6.0 Support the "patternProperties" keyword for objects.
+ *              Support the "anyOf" and "oneOf" keywords.
  *
  * @param array|object $data    The response data to modify.
  * @param array        $schema  The schema for the endpoint used to filter the response.
@@ -2041,6 +2888,28 @@
  * @return array|object The filtered response data.
  */
 function rest_filter_response_by_context( $data, $schema, $context ) {
+	if ( isset( $schema['anyOf'] ) ) {
+		$matching_schema = rest_find_any_matching_schema( $data, $schema, '' );
+		if ( ! is_wp_error( $matching_schema ) ) {
+			if ( ! isset( $schema['type'] ) ) {
+				$schema['type'] = $matching_schema['type'];
+			}
+
+			$data = rest_filter_response_by_context( $data, $matching_schema, $context );
+		}
+	}
+
+	if ( isset( $schema['oneOf'] ) ) {
+		$matching_schema = rest_find_one_matching_schema( $data, $schema, '', true );
+		if ( ! is_wp_error( $matching_schema ) ) {
+			if ( ! isset( $schema['type'] ) ) {
+				$schema['type'] = $matching_schema['type'];
+			}
+
+			$data = rest_filter_response_by_context( $data, $matching_schema, $context );
+		}
+	}
+
 	if ( ! is_array( $data ) && ! is_object( $data ) ) {
 		return $data;
 	}
@@ -2074,8 +2943,13 @@
 		} elseif ( $is_object_type ) {
 			if ( isset( $schema['properties'][ $key ] ) ) {
 				$check = $schema['properties'][ $key ];
-			} elseif ( $has_additional_properties ) {
-				$check = $schema['additionalProperties'];
+			} else {
+				$pattern_property_schema = rest_find_matching_pattern_property_schema( $key, $schema );
+				if ( null !== $pattern_property_schema ) {
+					$check = $pattern_property_schema;
+				} elseif ( $has_additional_properties ) {
+					$check = $schema['additionalProperties'];
+				}
 			}
 		}
 
@@ -2113,6 +2987,7 @@
  * Sets the "additionalProperties" to false by default for all object definitions in the schema.
  *
  * @since 5.5.0
+ * @since 5.6.0 Support the "patternProperties" keyword.
  *
  * @param array $schema The schema to modify.
  * @return array The modified schema.
@@ -2127,6 +3002,12 @@
 			}
 		}
 
+		if ( isset( $schema['patternProperties'] ) ) {
+			foreach ( $schema['patternProperties'] as $key => $child_schema ) {
+				$schema['patternProperties'][ $key ] = rest_default_additional_properties_to_false( $child_schema );
+			}
+		}
+
 		if ( ! isset( $schema['additionalProperties'] ) ) {
 			$schema['additionalProperties'] = false;
 		}
@@ -2214,7 +3095,7 @@
 	$route = '';
 
 	// The only controller that works is the Terms controller.
-	if ( 'WP_REST_Terms_Controller' === get_class( $controller ) ) {
+	if ( $controller instanceof WP_REST_Terms_Controller ) {
 		$namespace = 'wp/v2';
 		$rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
 		$route     = sprintf( '/%s/%s/%d', $namespace, $rest_base, $term->term_id );
@@ -2258,3 +3139,122 @@
 	 */
 	return apply_filters( 'rest_queried_resource_route', $route );
 }
+
+/**
+ * Retrieves an array of endpoint arguments from the item schema and endpoint method.
+ *
+ * @since 5.6.0
+ *
+ * @param array  $schema The full JSON schema for the endpoint.
+ * @param string $method Optional. HTTP method of the endpoint. The arguments for `CREATABLE` endpoints are
+ *                       checked for required values and may fall-back to a given default, this is not done
+ *                       on `EDITABLE` endpoints. Default WP_REST_Server::CREATABLE.
+ * @return array The endpoint arguments.
+ */
+function rest_get_endpoint_args_for_schema( $schema, $method = WP_REST_Server::CREATABLE ) {
+
+	$schema_properties       = ! empty( $schema['properties'] ) ? $schema['properties'] : array();
+	$endpoint_args           = array();
+	$valid_schema_properties = rest_get_allowed_schema_keywords();
+	$valid_schema_properties = array_diff( $valid_schema_properties, array( 'default', 'required' ) );
+
+	foreach ( $schema_properties as $field_id => $params ) {
+
+		// Arguments specified as `readonly` are not allowed to be set.
+		if ( ! empty( $params['readonly'] ) ) {
+			continue;
+		}
+
+		$endpoint_args[ $field_id ] = array(
+			'validate_callback' => 'rest_validate_request_arg',
+			'sanitize_callback' => 'rest_sanitize_request_arg',
+		);
+
+		if ( WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) {
+			$endpoint_args[ $field_id ]['default'] = $params['default'];
+		}
+
+		if ( WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) {
+			$endpoint_args[ $field_id ]['required'] = true;
+		}
+
+		foreach ( $valid_schema_properties as $schema_prop ) {
+			if ( isset( $params[ $schema_prop ] ) ) {
+				$endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ];
+			}
+		}
+
+		// Merge in any options provided by the schema property.
+		if ( isset( $params['arg_options'] ) ) {
+
+			// Only use required / default from arg_options on CREATABLE endpoints.
+			if ( WP_REST_Server::CREATABLE !== $method ) {
+				$params['arg_options'] = array_diff_key(
+					$params['arg_options'],
+					array(
+						'required' => '',
+						'default'  => '',
+					)
+				);
+			}
+
+			$endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] );
+		}
+	}
+
+	return $endpoint_args;
+}
+
+
+/**
+ * Converts an error to a response object.
+ *
+ * This iterates over all error codes and messages to change it into a flat
+ * array. This enables simpler client behaviour, as it is represented as a
+ * list in JSON rather than an object/map.
+ *
+ * @since 5.7.0
+ *
+ * @param WP_Error $error WP_Error instance.
+ *
+ * @return WP_REST_Response List of associative arrays with code and message keys.
+ */
+function rest_convert_error_to_response( $error ) {
+	$status = array_reduce(
+		$error->get_all_error_data(),
+		function ( $status, $error_data ) {
+			return is_array( $error_data ) && isset( $error_data['status'] ) ? $error_data['status'] : $status;
+		},
+		500
+	);
+
+	$errors = array();
+
+	foreach ( (array) $error->errors as $code => $messages ) {
+		$all_data  = $error->get_all_error_data( $code );
+		$last_data = array_pop( $all_data );
+
+		foreach ( (array) $messages as $message ) {
+			$formatted = array(
+				'code'    => $code,
+				'message' => $message,
+				'data'    => $last_data,
+			);
+
+			if ( $all_data ) {
+				$formatted['additional_data'] = $all_data;
+			}
+
+			$errors[] = $formatted;
+		}
+	}
+
+	$data = $errors[0];
+	if ( count( $errors ) > 1 ) {
+		// Remove the primary error.
+		array_shift( $errors );
+		$data['additional_errors'] = $errors;
+	}
+
+	return new WP_REST_Response( $data, $status );
+}