Commit f56906cf5d for wordpress.org
commit f56906cf5db83357acca2d456d7d05ca36726548
Author: gziolo <gziolo@git.wordpress.org>
Date: Tue Oct 20 09:10:57 2026 +0000
Abilities API: Preserve empty objects in JSON results
Empty PHP arrays representing objects are incorrectly encoded as []
instead of {}. Add wp_prepare_json_value_for_client() to prepare ability
results according to their output schemas before JSON encoding.
Apply it to AI client results and associative REST results, preserving
compatibility with _fields and _embed processing.
Props sukhendu2002, gziolo.
See #66267.
Built from https://develop.svn.wordpress.org/trunk@64592
git-svn-id: http://core.svn.wordpress.org/trunk@63742 1a063a9b-81f0-0310-95a4-ce76da25c4cd
diff --git a/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php b/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php
index 596656d36f..3c2bceed80 100644
--- a/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php
+++ b/wp-includes/ai-client/class-wp-ai-client-ability-function-resolver.php
@@ -148,6 +148,8 @@ class WP_AI_Client_Ability_Function_Resolver {
);
}
+ $result = wp_prepare_json_value_for_client( $result, $ability->get_output_schema() );
+
return new FunctionResponse(
$function_id,
$function_name,
diff --git a/wp-includes/json-schema.php b/wp-includes/json-schema.php
index bec3656f66..5b613ae46a 100644
--- a/wp-includes/json-schema.php
+++ b/wp-includes/json-schema.php
@@ -93,6 +93,116 @@ function wp_prepare_json_schema_for_client( array $schema, string $schema_profil
return _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema, $allowed_keywords );
}
+/**
+ * Prepares a JSON value for clients 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.
+ *
+ * @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.
+ */
+function wp_prepare_json_value_for_client( $value, array $schema ) {
+ if ( $value instanceof JsonSerializable ) {
+ $value = $value->jsonSerialize();
+ }
+
+ if ( ! is_array( $value ) && ! $value instanceof stdClass ) {
+ return $value;
+ }
+
+ foreach ( array( 'anyOf', 'oneOf' ) as $keyword ) {
+ if ( ! isset( $schema[ $keyword ] ) ) {
+ continue;
+ }
+
+ $matching_schema = null;
+
+ /*
+ * Prefer an array schema for PHP lists. An empty PHP array also matches an
+ * object schema, but should remain [] when an array schema accepts it.
+ */
+ if ( is_array( $value ) && array_is_list( $value ) ) {
+ foreach ( $schema[ $keyword ] as $candidate_schema ) {
+ if ( ! isset( $candidate_schema['type'] ) && isset( $schema['type'] ) ) {
+ $candidate_schema['type'] = $schema['type'];
+ }
+
+ if ( _wp_json_schema_matches_type( $value, $candidate_schema, 'array' ) ) {
+ $matching_schema = $candidate_schema;
+ break;
+ }
+ }
+ }
+
+ // Like rest_filter_response_by_context(), use the first matching oneOf branch.
+ if ( null === $matching_schema ) {
+ $matching_schema = 'anyOf' === $keyword
+ ? rest_find_any_matching_schema( $value, $schema, '' )
+ : rest_find_one_matching_schema( $value, $schema, '', true );
+ }
+
+ if ( is_wp_error( $matching_schema ) ) {
+ continue;
+ }
+
+ if ( ! isset( $schema['type'] ) && isset( $matching_schema['type'] ) ) {
+ $schema['type'] = $matching_schema['type'];
+ }
+
+ $value = wp_prepare_json_value_for_client( $value, $matching_schema );
+ }
+
+ $is_array_value = is_array( $value );
+ $types = isset( $schema['type'] ) ? (array) $schema['type'] : array();
+ $allows_object = in_array( 'object', $types, true );
+ $allows_array = in_array( 'array', $types, true );
+
+ if ( $is_array_value && empty( $value ) && $allows_object && ! $allows_array ) {
+ return (object) $value;
+ }
+
+ $is_object_value = $value instanceof stdClass || ( $is_array_value && $allows_object && ( ! $allows_array || ! array_is_list( $value ) ) );
+ if ( $allows_object && $is_object_value ) {
+ if ( $value instanceof stdClass ) {
+ $value = clone $value;
+ }
+
+ foreach ( $value as $property => $property_value ) {
+ $property_schema = null;
+ if ( isset( $schema['properties'][ $property ] ) && is_array( $schema['properties'][ $property ] ) ) {
+ $property_schema = $schema['properties'][ $property ];
+ } else {
+ $property_schema = rest_find_matching_pattern_property_schema( $property, $schema );
+ if ( null === $property_schema && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
+ $property_schema = $schema['additionalProperties'];
+ }
+ }
+
+ if ( null === $property_schema ) {
+ continue;
+ }
+
+ $property_value = wp_prepare_json_value_for_client( $property_value, $property_schema );
+ if ( is_array( $value ) ) {
+ $value[ $property ] = $property_value;
+ } else {
+ $value->$property = $property_value;
+ }
+ }
+ } elseif ( $allows_array && $is_array_value && isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
+ foreach ( $value as $index => $item ) {
+ $value[ $index ] = wp_prepare_json_value_for_client( $item, $schema['items'] );
+ }
+ }
+
+ return $value;
+}
+
/**
* Prepares a JSON Schema for clients using a given keyword lookup.
*
@@ -199,3 +309,56 @@ function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema
return $schema;
}
+
+/**
+ * Determines whether a value matches a JSON Schema through a given type.
+ *
+ * Combining schemas must have a matching branch that allows the requested type.
+ * Finding the type in one branch and validating the value against another branch
+ * is not sufficient.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @param mixed $value The value to validate.
+ * @param array<string, mixed> $schema The schema to inspect.
+ * @param string $type The type to match.
+ * @return bool Whether the value matches the schema through the requested type.
+ */
+function _wp_json_schema_matches_type( $value, array $schema, string $type ): bool {
+ if ( isset( $schema['type'] ) && ! in_array( $type, (array) $schema['type'], true ) ) {
+ return false;
+ }
+
+ foreach ( array( 'anyOf', 'oneOf' ) as $keyword ) {
+ if ( ! isset( $schema[ $keyword ] ) ) {
+ continue;
+ }
+
+ foreach ( $schema[ $keyword ] as $sub_schema ) {
+ if ( ! is_array( $sub_schema ) ) {
+ continue;
+ }
+
+ // Match rest_find_any_matching_schema(), which applies the parent type to untyped branches.
+ if ( ! isset( $sub_schema['type'] ) && isset( $schema['type'] ) ) {
+ $sub_schema['type'] = $schema['type'];
+ }
+
+ if ( _wp_json_schema_matches_type( $value, $sub_schema, $type ) ) {
+ continue 2;
+ }
+ }
+
+ return false;
+ }
+
+ if ( ! isset( $schema['type'] ) && ! isset( $schema['anyOf'] ) && ! isset( $schema['oneOf'] ) ) {
+ return false;
+ }
+
+ // Explicit and inferred types must both validate through the requested type only.
+ $schema['type'] = $type;
+
+ return ! is_wp_error( rest_validate_value_from_schema( $value, $schema ) );
+}
diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
index 9ba76fc240..12f6a75083 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-abilities-v1-run-controller.php
@@ -95,6 +95,20 @@ class WP_REST_Abilities_V1_Run_Controller extends WP_REST_Controller {
return $result;
}
+ /*
+ * Preserve empty objects in nested properties when encoding an associative
+ * result as JSON. Preparation can convert empty PHP arrays to stdClass.
+ *
+ * The REST server's `_fields` and `_embed` processing expects arrays at the
+ * response root and for numeric collection items. Converting those values
+ * to objects would cause type errors, so skip root empty arrays and numeric
+ * collections until that processing supports objects. An empty object result
+ * therefore still encodes as [], and a list containing one encodes as [[]].
+ */
+ if ( is_array( $result ) && ! wp_is_numeric_array( $result ) ) {
+ $result = wp_prepare_json_value_for_client( $result, $ability->get_output_schema() );
+ }
+
return rest_ensure_response( $result );
}
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 86ff5de376..cd076fb42c 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
*
* @global string $wp_version
*/
-$wp_version = '7.2-alpha-64591';
+$wp_version = '7.2-alpha-64592';
/**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.