[wp-trac] [WordPress Trac] #64926: REST API: GET requests fail object/array schema validation when params are JSON-serialized strings

WordPress Trac noreply at wordpress.org
Tue Apr 7 17:31:23 UTC 2026


#64926: REST API: GET requests fail object/array schema validation when params are
JSON-serialized strings
-------------------------+-------------------------------------------------
 Reporter:  dsmy         |       Owner:  (none)
     Type:  defect       |      Status:  new
  (bug)                  |
 Priority:  normal       |   Milestone:  Awaiting Review
Component:  REST API     |     Version:
 Severity:  normal       |  Resolution:
 Keywords:  has-patch    |     Focuses:  javascript, rest-api, php-
                         |  compatibility
-------------------------+-------------------------------------------------

Comment (by dsmy):

 @westonruter I tested both encoding paths against two schema shapes on
 WordPress trunk (unpatched):

 **Schema A —** fully-typed properties (your example shape):
 addQueryArgs() bracket notation, booleans preserved.
 rest_sanitize_value_from_schema() coerces correctly when property types
 are declared. You're right that this works today.

 JSON.stringify gives 400 rest_invalid_type (the bug, unpatched).

 **Schema B —** additionalProperties: true, no declared property types (our
 use case):
 addQueryArgs() bracket notation are accepted, but types collapse: { lock:
 { move: false, remove: true }, dropCap: false } arrives as { lock: { move:
 "false", remove: "true" }, dropCap: "false" }. No schema type hints no
 coercion.

 JSON.stringify gives 400 rest_invalid_type (same bug).

 So addQueryArgs() solves the problem for endpoints where every nested
 property is typed in the schema. For additionalProperties schemas and for
 block attribute filters where you can't pre-declare every possible key
 bracket notation is lossy and JSON.stringify is the only lossless encoding
 path.

 This confirms the patch is still needed. It closes the gap for both schema
 shapes, consistent with how parse_json_params() already handles POST
 bodies.

 Replying to [comment:25 westonruter]:
 > Something that just came to mind: the abilities in the Abilities API can
 be exposed in the REST API. When an ability is read-only, then a `GET`
 request is made. Nevertheless, the `input_schema` may involve an object of
 properties.
 >
 > Consider this Abilities API code:
 >
 > {{{#!php
 > <?php
 > add_action(
 >       'wp_abilities_api_init',
 >       static function () {
 >               wp_register_ability(
 >                       'abilities-experiment/get-post',
 >                       array(
 >                               'label'               => __( 'Get Post',
 'abilities-experiment' ),
 >                               'description'         => __( 'Gets fields
 for a post', 'abilities-experiment' ),
 >                               'category'            => 'site',
 >                               'input_schema'        => array(
 >                                       'type'                 =>
 'object',
 >                                       'required'             => array(
 'post' ),
 >                                       'properties'           => array(
 >                                               'post' => array(
 >                                                       'type' =>
 'object',
 >                                                       'required' =>
 array( 'id' ),
 >                                                       'properties' =>
 array(
 >                                                               'id' =>
 array(
 >
 'type'        => 'integer',
 >
 'description' => __( 'The ID of the post to get.', 'abilities-experiment'
 ),
 >
 'minimum'     => 1,
 >                                                               ),
 >                                                       )
 >                                               ),
 >                                               'fields' => array(
 >                                                       'type' => 'array',
 >                                                       'items' => array(
 >                                                               'type' =>
 'string',
 >                                                       ),
 >                                               )
 >                                       ),
 >                               ),
 >                               'output_schema'       => array(
 >                                       'type'                 =>
 'object',
 >                               ),
 >                               'execute_callback'    => static function (
 array $input ): array|WP_Error {
 >                                       $post = get_post(
 $input['post']['id'] );
 >                                       if ( ! $post ) {
 >                                               return new WP_Error(
 'post_not_found', __( 'Post not found.', 'abilities-experiment' ), array(
 'status' => 404 ) );
 >                                       }
 >                                       $data = $post->to_array();
 >                                       if ( $input['fields'] ) {
 >                                               $data =
 wp_array_slice_assoc( $data, $input['fields'] );
 >                                       }
 >                                       return $data;
 >                               },
 >                               'permission_callback' => static function (
 array $input ): bool {
 >                                       return current_user_can(
 'edit_posts', $input['post']['id'] );
 >                               },
 >                               'meta'                => array(
 >                                       'annotations'  => array(
 >                                               'readonly'    => true,
 >                                               'destructive' => false,
 >                                               'idempotent'  => false,
 >                                       ),
 >                                       'show_in_rest' => true,
 >                               ),
 >                       )
 >               );
 >       }
 > );
 > }}}
 >
 > Using the Abilities API client module as follows:
 >
 > {{{#!js
 > (await import("@wordpress/abilities")).executeAbility(
 >   "abilities-experiment/get-post",
 >   { post: { id: 1 }, fields: ["post_content", "post_author"] },
 > );
 > }}}
 >
 > This results in an HTTP request being made to the REST API as follows:
 >
 > `/wp-json/wp-abilities/v1/abilities/abilities-experiment/get-
 post/run?input%5Bpost%5D%5Bid%5D=1&input%5Bfields%5D%5B0%5D=post_content&input%5Bfields%5D%5B1%5D=post_author&_locale=user`
 >
 > With the brackets decoded:
 >
 > `/wp-json/wp-abilities/v1/abilities/abilities-experiment/get-
 post/run?input[post][id]=1&input[fields][0]=post_content&input[fields][1]=post_author&_locale=user`
 >
 > So, note that `@wordpress/abilities` (er, `@wordpress/core-abilities`)
 is already handling this correctly, converting a nested object into a list
 of URL query params.
 >
 > See
 [https://github.com/WordPress/gutenberg/blob/6ea168835304764d253d169906148cf5fe6269d9/packages
 /core-abilities/src/index.ts#L27-L71 logic] in `createServerCallback()`:
 >
 > {{{
 > // For GET and DELETE requests, pass the input as query parameters.
 > path = addQueryArgs( path, { input } );
 > }}}
 >
 > This `addQueryArgs()` function is part of the `@wordpress/url` package:
 https://developer.wordpress.org/block-editor/reference-guides/packages
 /packages-url/#addqueryargs
 >
 > So you can use that instead of the `qs` package.

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/64926#comment:26>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list