Commit 67e770f4eb for wordpress.org
commit 67e770f4eb381da514a8790834e5de27e7c70bea
Author: gziolo <gziolo@git.wordpress.org>
Date: Tue Oct 20 10:08:53 2026 +0000
JSON Schema: Prepare default values according to their schemas
Use wp_prepare_json_value_for_client() to normalize existing schema defaults, preserving empty objects within nested values and resolving matching anyOf and oneOf branches. Defaults remain arrays when a matching schema allows arrays.
Add regression coverage for nested defaults and clarify the schema and value preparation PHPDoc.
Follow-up to [64592], [64315].
Developed in https://github.com/WordPress/wordpress-develop/pull/14542.
Fix #66267.
Built from https://develop.svn.wordpress.org/trunk@64594
git-svn-id: http://core.svn.wordpress.org/trunk@63744 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/json-schema.php b/wp-includes/json-schema.php
index 5b613ae46a..74206bfcf3 100644
--- a/wp-includes/json-schema.php
+++ b/wp-includes/json-schema.php
@@ -71,13 +71,12 @@ function wp_get_json_schema_allowed_keywords( string $schema_profile = 'rest-api
/**
* Prepares a JSON Schema for clients.
*
- * Use this before exposing a schema outside of WordPress's server-side
- * validation, for example in REST responses, Ability metadata, or AI provider
- * requests. The prepared schema uses forms that JSON Schema draft-04 clients
- * can understand.
+ * Converts WordPress-specific schema conventions and removes unsupported
+ * keywords so clients can interpret the schema using the selected profile.
*
- * WordPress-internal schema conveniences are converted or removed only where
- * needed to keep the exposed schema valid for the selected profile.
+ * Applies the same preparation to nested schemas. Existing default values
+ * are normalized according to their schemas so empty objects encode as `{}`
+ * rather than `[]`, including within nested default values.
*
* @since 7.1.0
*
@@ -94,17 +93,130 @@ function wp_prepare_json_schema_for_client( array $schema, string $schema_profil
}
/**
- * Prepares a JSON value for clients according to its schema.
+ * Prepares a JSON Schema for clients using a given keyword lookup.
+ *
+ * Normalizes schema conventions and default values throughout nested schemas
+ * while retaining only the allowed keywords.
+ *
+ * @since 7.1.0
+ * @access private
+ *
+ * @param array<string, mixed> $schema The schema array.
+ * @param array<string, true> $allowed_keywords Lookup map of allowed JSON Schema keywords.
+ * @return array<string, mixed> The prepared schema.
+ */
+function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema, array $allowed_keywords ): array {
+ if ( array_key_exists( 'default', $schema ) ) {
+ $schema['default'] = wp_prepare_json_value_for_client( $schema['default'], $schema );
+ }
+
+ $schema = array_intersect_key( $schema, $allowed_keywords );
+
+ /*
+ * Collect draft-03 per-property `required: true` flags into a draft-04
+ * `required` array of property names on the parent object schema.
+ *
+ * This mirrors rest_validate_object_value_from_schema(), where a draft-04
+ * `required` array takes precedence: when one is present, per-property
+ * booleans are ignored during validation. They are therefore left out of
+ * the array here as well (but still stripped from the output) so the
+ * published schema describes exactly what gets enforced.
+ */
+ if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
+ $has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
+ $required = array();
+ foreach ( $schema['properties'] as $property => &$property_schema ) {
+ if ( is_array( $property_schema ) && ! wp_is_numeric_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
+ if ( ! $has_required_array && true === $property_schema['required'] ) {
+ $required[] = (string) $property;
+ }
+ unset( $property_schema['required'] );
+ }
+ }
+ unset( $property_schema );
+
+ /*
+ * Property keys are unique, so the collected list needs no deduplication.
+ * When a draft-04 array is already present, leave it untouched.
+ */
+ if ( ! $has_required_array && count( $required ) > 0 ) {
+ $schema['required'] = $required;
+ }
+ }
+
+ /*
+ * A boolean `required` outside of an object's property list has no draft-04
+ * equivalent, so drop it rather than emit an invalid keyword.
+ */
+ if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
+ unset( $schema['required'] );
+ }
+
+ /*
+ * Sub-schema maps: keys are user-defined, values are sub-schemas.
+ * Note: 'dependencies' values can also be property-dependency arrays
+ * (numeric arrays of strings) which are skipped via wp_is_numeric_array().
+ */
+ foreach ( array( 'properties', 'patternProperties', 'definitions', 'dependencies' ) as $keyword ) {
+ if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
+ foreach ( $schema[ $keyword ] as $key => $child_schema ) {
+ if ( is_array( $child_schema ) && ! wp_is_numeric_array( $child_schema ) ) {
+ $schema[ $keyword ][ $key ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $child_schema, $allowed_keywords );
+ }
+ }
+ }
+ }
+
+ // Single sub-schema keywords.
+ foreach ( array( 'not', 'additionalProperties', 'additionalItems' ) as $keyword ) {
+ if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) && ! wp_is_numeric_array( $schema[ $keyword ] ) ) {
+ $schema[ $keyword ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema[ $keyword ], $allowed_keywords );
+ }
+ }
+
+ // Items: single schema or tuple array of schemas.
+ if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
+ if ( ! wp_is_numeric_array( $schema['items'] ) ) {
+ $schema['items'] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema['items'], $allowed_keywords );
+ } else {
+ foreach ( $schema['items'] as $index => $item_schema ) {
+ if ( is_array( $item_schema ) && ! wp_is_numeric_array( $item_schema ) ) {
+ $schema['items'][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $item_schema, $allowed_keywords );
+ }
+ }
+ }
+ }
+
+ // Array-of-schemas keywords.
+ foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $keyword ) {
+ if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
+ foreach ( $schema[ $keyword ] as $index => $sub_schema ) {
+ if ( is_array( $sub_schema ) && ! wp_is_numeric_array( $sub_schema ) ) {
+ $schema[ $keyword ][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $sub_schema, $allowed_keywords );
+ }
+ }
+ }
+ }
+
+ return $schema;
+}
+
+/**
+ * Prepares a value for JSON encoding according to its schema.
*
- * Empty PHP arrays are converted to objects when the schema allows objects but
- * not arrays. Nested values are prepared using matching anyOf or oneOf branches
- * and their property or item schemas.
+ * Preserves the distinction between JSON objects and arrays by converting
+ * empty PHP arrays to objects where the schema identifies them as objects.
+ * Nested properties and items are prepared according to their schemas.
+ * Empty arrays remain arrays when a matching schema allows them.
+ *
+ * Prepares the representation of existing values without inserting defaults
+ * or performing full JSON Schema validation.
*
* @since 7.2.0
*
* @param mixed $value The value to prepare.
* @param array<string, mixed> $schema The schema describing the value.
- * @return mixed The prepared value.
+ * @return mixed The value prepared for JSON encoding.
*/
function wp_prepare_json_value_for_client( $value, array $schema ) {
if ( $value instanceof JsonSerializable ) {
@@ -203,113 +315,6 @@ function wp_prepare_json_value_for_client( $value, array $schema ) {
return $value;
}
-/**
- * Prepares a JSON Schema for clients using a given keyword lookup.
- *
- * @since 7.1.0
- * @access private
- *
- * @param array<string, mixed> $schema The schema array.
- * @param array<string, true> $allowed_keywords Lookup map of allowed JSON Schema keywords.
- * @return array<string, mixed> The prepared schema.
- */
-function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema, array $allowed_keywords ): array {
- $types = isset( $schema['type'] ) ? (array) $schema['type'] : array();
- if ( isset( $schema['default'] ) && array() === $schema['default'] && in_array( 'object', $types, true ) && ! in_array( 'array', $types, true ) ) {
- $schema['default'] = new stdClass();
- }
-
- $schema = array_intersect_key( $schema, $allowed_keywords );
-
- /*
- * Collect draft-03 per-property `required: true` flags into a draft-04
- * `required` array of property names on the parent object schema.
- *
- * This mirrors rest_validate_object_value_from_schema(), where a draft-04
- * `required` array takes precedence: when one is present, per-property
- * booleans are ignored during validation. They are therefore left out of
- * the array here as well (but still stripped from the output) so the
- * published schema describes exactly what gets enforced.
- */
- if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
- $has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
- $required = array();
- foreach ( $schema['properties'] as $property => &$property_schema ) {
- if ( is_array( $property_schema ) && ! wp_is_numeric_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
- if ( ! $has_required_array && true === $property_schema['required'] ) {
- $required[] = (string) $property;
- }
- unset( $property_schema['required'] );
- }
- }
- unset( $property_schema );
-
- /*
- * Property keys are unique, so the collected list needs no deduplication.
- * When a draft-04 array is already present, leave it untouched.
- */
- if ( ! $has_required_array && count( $required ) > 0 ) {
- $schema['required'] = $required;
- }
- }
-
- /*
- * A boolean `required` outside of an object's property list has no draft-04
- * equivalent, so drop it rather than emit an invalid keyword.
- */
- if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
- unset( $schema['required'] );
- }
-
- /*
- * Sub-schema maps: keys are user-defined, values are sub-schemas.
- * Note: 'dependencies' values can also be property-dependency arrays
- * (numeric arrays of strings) which are skipped via wp_is_numeric_array().
- */
- foreach ( array( 'properties', 'patternProperties', 'definitions', 'dependencies' ) as $keyword ) {
- if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
- foreach ( $schema[ $keyword ] as $key => $child_schema ) {
- if ( is_array( $child_schema ) && ! wp_is_numeric_array( $child_schema ) ) {
- $schema[ $keyword ][ $key ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $child_schema, $allowed_keywords );
- }
- }
- }
- }
-
- // Single sub-schema keywords.
- foreach ( array( 'not', 'additionalProperties', 'additionalItems' ) as $keyword ) {
- if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) && ! wp_is_numeric_array( $schema[ $keyword ] ) ) {
- $schema[ $keyword ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema[ $keyword ], $allowed_keywords );
- }
- }
-
- // Items: single schema or tuple array of schemas.
- if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
- if ( ! wp_is_numeric_array( $schema['items'] ) ) {
- $schema['items'] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema['items'], $allowed_keywords );
- } else {
- foreach ( $schema['items'] as $index => $item_schema ) {
- if ( is_array( $item_schema ) && ! wp_is_numeric_array( $item_schema ) ) {
- $schema['items'][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $item_schema, $allowed_keywords );
- }
- }
- }
- }
-
- // Array-of-schemas keywords.
- foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $keyword ) {
- if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
- foreach ( $schema[ $keyword ] as $index => $sub_schema ) {
- if ( is_array( $sub_schema ) && ! wp_is_numeric_array( $sub_schema ) ) {
- $schema[ $keyword ][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $sub_schema, $allowed_keywords );
- }
- }
- }
- }
-
- return $schema;
-}
-
/**
* Determines whether a value matches a JSON Schema through a given type.
*
diff --git a/wp-includes/version.php b/wp-includes/version.php
index b6a87b7295..5d977dbe38 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-64593';
+$wp_version = '7.2-alpha-64594';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.