From e2d299b2e00fa31a2e40985d0eb78becc85235c3 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 20 Oct 2026 14:47:47 +0100 Subject: [PATCH] REST API: Use `wp_prepare_json_value_for_client()` for view configs and entity fields. Remove the private `_rest_cast_empty_objects_from_schema()` helper, which `wp_prepare_json_value_for_client()` supersedes. The public function also recurses into `oneOf`/`anyOf` branches, `patternProperties`, objects and `JsonSerializable` values, and keeps an empty array as `[]` when the schema also allows an array. Follow-up to [64503], [64592]. See #66267. --- src/wp-includes/rest-api.php | 74 -------- ...class-wp-rest-entity-fields-controller.php | 2 +- .../class-wp-rest-view-config-controller.php | 14 +- .../restCastEmptyObjectsFromSchema.php | 163 ------------------ 4 files changed, 8 insertions(+), 245 deletions(-) delete mode 100644 tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 4ec8f119f36e0..9998e14da7361 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -3235,80 +3235,6 @@ function rest_default_additional_properties_to_false( $schema ) { return $schema; } -/** - * Recursively casts empty arrays to objects where the schema types them as objects. - * - * PHP cannot distinguish an empty associative array from an empty list, so - * `json_encode()` always serializes `array()` as a JSON array (`[]`). A REST - * schema, however, may type a value as an object, which must encode as `{}`. - * This walks the value against its schema and casts any empty, object-typed - * array to an object. Non-empty associative arrays already encode as objects, - * so they are left as arrays and only recursed into to fix any nested empty - * objects. - * - * Union schemas (`oneOf`/`anyOf`) are handled only for the empty-array case: - * an empty value is cast to an object when any branch allows an object. Such - * values are not recursed into. - * - * @since 7.2.0 - * @access private - * - * @param mixed $value The value to normalize. - * @param array $schema The schema node describing the value. - * @return mixed The normalized value, with empty object-typed arrays cast to objects. - */ -function _rest_cast_empty_objects_from_schema( $value, $schema ) { - if ( ! is_array( $value ) || ! is_array( $schema ) ) { - return $value; - } - - if ( isset( $schema['oneOf'] ) || isset( $schema['anyOf'] ) ) { - $branches = $schema['oneOf'] ?? $schema['anyOf']; - if ( array() === $value ) { - foreach ( $branches as $branch ) { - if ( is_array( $branch ) && in_array( 'object', (array) ( $branch['type'] ?? array() ), true ) ) { - return (object) array(); - } - } - } - return $value; - } - - $types = (array) ( $schema['type'] ?? array() ); - - if ( in_array( 'array', $types, true ) && isset( $schema['items'] ) ) { - foreach ( $value as $index => $item ) { - $value[ $index ] = _rest_cast_empty_objects_from_schema( $item, $schema['items'] ); - } - return $value; - } - - if ( in_array( 'object', $types, true ) ) { - if ( isset( $schema['properties'] ) ) { - foreach ( $schema['properties'] as $property => $property_schema ) { - if ( array_key_exists( $property, $value ) ) { - $value[ $property ] = _rest_cast_empty_objects_from_schema( $value[ $property ], $property_schema ); - } - } - } - if ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) { - foreach ( $value as $key => $item ) { - if ( isset( $schema['properties'][ $key ] ) ) { - continue; - } - $value[ $key ] = _rest_cast_empty_objects_from_schema( $item, $schema['additionalProperties'] ); - } - } - - // Empty object-typed arrays must serialize as {} to match the schema. - if ( array() === $value ) { - return (object) array(); - } - } - - return $value; -} - /** * Gets the REST API route for a post. * diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-fields-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-fields-controller.php index dae68fff62106..e7674e2a80ba7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-fields-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-entity-fields-controller.php @@ -156,7 +156,7 @@ public function get_items( $request ) { $field_schema = $this->get_field_schema(); $fields = array(); foreach ( wp_get_registered_entity_fields( $kind, $name ) as $field ) { - $fields[] = _rest_cast_empty_objects_from_schema( $field, $field_schema ); + $fields[] = wp_prepare_json_value_for_client( $field, $field_schema ); } $script_modules = array(); diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php index 44de983ab4300..7da61b0486868 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-view-config-controller.php @@ -155,10 +155,10 @@ public function get_items( $request ) { 'kind' => $kind, 'name' => $name, 'version' => WP_View_Config_Data::LATEST_VERSION, - 'default_view' => _rest_cast_empty_objects_from_schema( $config['default_view'], $schema['properties']['default_view'] ), - 'default_layouts' => _rest_cast_empty_objects_from_schema( $config['default_layouts'], $schema['properties']['default_layouts'] ), - 'view_list' => _rest_cast_empty_objects_from_schema( $config['view_list'], $schema['properties']['view_list'] ), - 'form' => _rest_cast_empty_objects_from_schema( $config['form'], $schema['properties']['form'] ), + 'default_view' => wp_prepare_json_value_for_client( $config['default_view'], $schema['properties']['default_view'] ), + 'default_layouts' => wp_prepare_json_value_for_client( $config['default_layouts'], $schema['properties']['default_layouts'] ), + 'view_list' => wp_prepare_json_value_for_client( $config['view_list'], $schema['properties']['view_list'] ), + 'form' => wp_prepare_json_value_for_client( $config['form'], $schema['properties']['form'] ), ); return rest_ensure_response( $response ); @@ -169,16 +169,16 @@ public function get_items( $request ) { * objects. * * @since 7.1.0 - * @since 7.2.0 Moved the logic to _rest_cast_empty_objects_from_schema(). + * @since 7.2.0 Uses wp_prepare_json_value_for_client(). * - * @see _rest_cast_empty_objects_from_schema() + * @see wp_prepare_json_value_for_client() * * @param mixed $value The value to normalize. * @param array $schema The schema node describing the value. * @return mixed The normalized value, with empty object-typed arrays cast to objects. */ protected function cast_empty_objects( $value, $schema ) { - return _rest_cast_empty_objects_from_schema( $value, $schema ); + return is_array( $schema ) ? wp_prepare_json_value_for_client( $value, $schema ) : $value; } /** diff --git a/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php b/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php deleted file mode 100644 index e5a2546733b4c..0000000000000 --- a/tests/phpunit/tests/rest-api/restCastEmptyObjectsFromSchema.php +++ /dev/null @@ -1,163 +0,0 @@ -assertSame( '{}', $this->cast_and_encode( array(), array( 'type' => 'object' ) ) ); - } - - public function test_empty_array_encodes_as_array() { - $this->assertSame( '[]', $this->cast_and_encode( array(), array( 'type' => 'array' ) ) ); - } - - public function test_empty_value_with_object_in_type_list_encodes_as_object() { - $this->assertSame( '{}', $this->cast_and_encode( array(), array( 'type' => array( 'object', 'boolean' ) ) ) ); - } - - public function test_non_array_values_are_returned_unchanged() { - $this->assertFalse( _rest_cast_empty_objects_from_schema( false, array( 'type' => array( 'object', 'boolean' ) ) ) ); - $this->assertSame( 'text', _rest_cast_empty_objects_from_schema( 'text', array( 'type' => 'string' ) ) ); - } - - public function test_value_without_schema_is_returned_unchanged() { - $this->assertSame( array(), _rest_cast_empty_objects_from_schema( array(), array() ) ); - } - - public function test_nested_properties_are_cast() { - $schema = array( - 'type' => 'object', - 'properties' => array( - 'layout' => array( - 'type' => 'object', - 'properties' => array( - 'styles' => array( 'type' => 'object' ), - ), - ), - 'fields' => array( 'type' => 'array' ), - ), - ); - - $this->assertSame( - '{"layout":{"styles":{}},"fields":[]}', - $this->cast_and_encode( - array( - 'layout' => array( 'styles' => array() ), - 'fields' => array(), - ), - $schema - ) - ); - } - - public function test_object_emptied_of_known_properties_is_still_cast() { - $schema = array( - 'type' => 'object', - 'properties' => array( - 'layout' => array( 'type' => 'object' ), - ), - ); - - $this->assertSame( '{}', $this->cast_and_encode( array(), $schema ) ); - } - - public function test_array_items_are_cast() { - $schema = array( - 'type' => 'array', - 'items' => array( 'type' => 'object' ), - ); - - $this->assertSame( '[{},{}]', $this->cast_and_encode( array( array(), array() ), $schema ) ); - } - - public function test_additional_properties_are_cast() { - $schema = array( - 'type' => 'object', - 'properties' => array( - 'known' => array( 'type' => 'array' ), - ), - 'additionalProperties' => array( 'type' => 'object' ), - ); - - $this->assertSame( - '{"known":[],"title":{}}', - $this->cast_and_encode( - array( - 'known' => array(), - 'title' => array(), - ), - $schema - ) - ); - } - - public function test_boolean_additional_properties_are_left_alone() { - $schema = array( - 'type' => 'object', - 'additionalProperties' => true, - ); - - $this->assertSame( '{"extra":[]}', $this->cast_and_encode( array( 'extra' => array() ), $schema ) ); - } - - /** - * @dataProvider data_union_keywords - * - * @param string $keyword The union keyword, `oneOf` or `anyOf`. - */ - public function test_empty_value_is_cast_when_a_union_branch_allows_an_object( $keyword ) { - $schema = array( - $keyword => array( - array( 'type' => 'string' ), - array( 'type' => 'object' ), - ), - ); - - $this->assertSame( '{}', $this->cast_and_encode( array(), $schema ) ); - } - - /** - * @dataProvider data_union_keywords - * - * @param string $keyword The union keyword, `oneOf` or `anyOf`. - */ - public function test_empty_value_is_not_cast_when_no_union_branch_allows_an_object( $keyword ) { - $schema = array( - $keyword => array( - array( 'type' => 'string' ), - array( 'type' => 'array' ), - ), - ); - - $this->assertSame( '[]', $this->cast_and_encode( array(), $schema ) ); - } - - /** - * Data provider. - * - * @return array[] - */ - public function data_union_keywords() { - return array( - 'oneOf' => array( 'oneOf' ), - 'anyOf' => array( 'anyOf' ), - ); - } -}