Skip to content

Abilities API: Encode empty object results as JSON objects - #14547

Open
jorgefilipecosta wants to merge 5 commits into
WordPress:trunkfrom
jorgefilipecosta:fix/abilities-empty-object-result
Open

jorgefilipecosta wants to merge 5 commits into
WordPress:trunkfrom
jorgefilipecosta:fix/abilities-empty-object-result

Conversation

@jorgefilipecosta

@jorgefilipecosta jorgefilipecosta commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Follow up to https://core.trac.wordpress.org/changeset/64592 and #14139.

Depends on #14671, whose two commits are the first ones here.

What

  • The run endpoint prepares every result for JSON clients with wp_prepare_json_value_for_client(), not only associative arrays. An empty array returned for an object output schema is now encoded as {} at the root, in lists, and inside object or JsonSerializable results, instead of [], which the JS client (executeAbility()) rejected with ability_invalid_output. This fixes it for all abilities, e.g. core/settings-get when no setting matches.
  • Arrays stay arrays: an empty list or a list of strings is still encoded as [].
  • The run handler sets the response_processing route option to false (from REST API: Let routes turn off _fields, _embed and response links #14671), so _fields, _embed and response links leave the result unchanged, and it follows the ability's output schema whether it is an object, an array, or a scalar value.
await wp.apiFetch( { path: wp.url.addQueryArgs( '/wp-abilities/v1/abilities/core/settings-get/run', { input: { group: 'writing', fields: [ 'title' ] } } ) } );
// Before: []
// After:  {}

await wp.apiFetch( { path: wp.url.addQueryArgs( '/wp-abilities/v1/abilities/core/settings-get/run', { input: { group: 'writing', fields: [ 'title' ] }, _fields: 'title' } ) } );
// After:  {}

Issue found while testing the query abilities through the JS client; the fix was drafted with AI assistance and reviewed by me.

Trac ticket: https://core.trac.wordpress.org/ticket/66267

Testing

Verify unit tests are passing:

npm run test:php -- --filter 'test_output_is_prepared_for_json_clients|test_no_matching_setting_is_encoded_as_an_empty_object'

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Core Committers: Use this line as a base for the props when committing in SVN:

Props jorgefilipecosta, gziolo.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@jorgefilipecosta jorgefilipecosta changed the title Abilities API: Preserve empty object results in REST responses Abilities API: Return an empty object from core/settings-get when no setting matches Oct 20, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from 8e657ba to 29c2ab8 Compare October 20, 2026 13:05
@gziolo

gziolo commented Oct 20, 2026

Copy link
Copy Markdown
Member

I noted in #14139 that we should follow-up and figure out how to map [] to {} in a way that doesn't break REST API handler when global handlers like _fields or _links get passed. Casting to (object) in the ability doesn't resolve the problem.

@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from 29c2ab8 to 0437a0b Compare October 20, 2026 13:26
@jorgefilipecosta jorgefilipecosta changed the title Abilities API: Return an empty object from core/settings-get when no setting matches Abilities API: Encode empty object results as JSON objects Oct 20, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from 0437a0b to e68fd10 Compare October 20, 2026 13:33
@jorgefilipecosta jorgefilipecosta changed the title Abilities API: Encode empty object results as JSON objects Abilities API: Return an empty object from core/settings-get when no setting matches Oct 20, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from e68fd10 to d05b08b Compare October 20, 2026 13:51
@jorgefilipecosta jorgefilipecosta changed the title Abilities API: Return an empty object from core/settings-get when no setting matches Abilities API: Encode empty object results as JSON objects Oct 20, 2026
@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from d05b08b to 9d63d51 Compare October 20, 2026 14:12
@jorgefilipecosta

Copy link
Copy Markdown
Member Author

I noted in #14139 that we should follow-up and figure out how to map [] to {} in a way that doesn't break REST API handler when global handlers like _fields or _links get passed. Casting to (object) in the ability doesn't resolve the problem.

Hi @gziolo this PR was updated and addressed these issues.

@gziolo

gziolo commented Oct 21, 2026

Copy link
Copy Markdown
Member

Could we let controllers explicitly opt out of _fields filtering, _embed processing, and response link injection?

The problem extends beyond objects to scalar values, where these transformations have no meaningful application. Currently, _fields can trigger a type error, embedding silently does nothing, and link injection can fail or change the response’s type. Broadening array-specific helpers to accept arbitrary values doesn’t establish consistent support for these features.

The ability execution controller could disable all three explicitly, preserving the ability’s output schema and making behavior predictable regardless of whether the result is an object, array, or scalar.

…nks.

Add the `allow_fields`, `allow_embed` and `allow_links` route and
handler options. When an option is false, the `_fields` parameter does
not filter the response data, the `_embed` parameter does not embed
linked resources, or the response links are not added to the response
data as `_links`. A handler option takes precedence over the route
option, as with `allow_batch`.

This lets a route serve its response data as returned, for example data
that follows a schema of its own or that is not an array.

See #66267.
@jorgefilipecosta

Copy link
Copy Markdown
Member Author

Could we let controllers explicitly opt out of _fields filtering, _embed processing, and response link injection?

Hi @gziolo, Currently there is no way to opt out, following your feedback I'm proposing it at #14671.

@gziolo gziolo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few suggestions to make the test cases easier to follow and strengthen coverage of the response-preservation behavior.


$data = array();
foreach ( $results as $label => $result ) {
foreach ( array( 'none', '_fields', '_embed', 'link', 'embedded link' ) as $global_param ) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$global_param contains both query parameter names and instructions for setting up the test, such as link and embedded link. This makes the cases harder to follow.

Could we consider passing query parameters directly from the provider, for example array( '_fields' => 'other', '_embed' => '1' ), and applying them with $request->set_query_params()? Link setup could be a separate argument. This would also make it easy to test both parameters together.

add_filter(
'rest_post_dispatch',
static function ( $response ) {
$response->add_link( 'alternate', rest_url( '/wp-abilities/v1/abilities' ), array( 'embeddable' => true ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test adds a link to the response metadata. Since link injection is disabled, that link never reaches the response body. The test therefore does not check what happens when the ability’s output already contains _links.

Could we consider adding a case where the ability returns its own _links property with an embeddable link? With _embed=1, we could check that the output stays unchanged and no request is made to fetch the linked resource.


add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 );
$response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request );
$data = $this->server->response_to_data( $response, isset( $global_params['_embed'] ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test manually calls the steps that process and encode the response. It checks those steps, but it does not check the body sent by serve_request().

Could we consider using serve_request() for this regression test, passing the input and query parameters through the normal request path and checking the emitted JSON? We could verify that the body is {} when _fields and _embed are supplied together. The broader output-shape tests could keep their current approach.

'numeric collection stays unchanged' => array( 'test/empty-object-list-output', '[[]]' ),
);
$results = array(
'root empty object' => array( 'test/empty-object-output', '{}' ),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case checks that a root empty object is encoded as {} when the response is served. Preloaded requests take a different path. rest_preload_api_request() casts the data with (array), and that turns the stdClass back into an empty array. So the preloaded body becomes [], and a scalar like "value" becomes ["value"].

Could we consider adding a preload test for a read-only ability (preloading only allows GET) that returns an empty object and a scalar? Today it would fail for both. The fix could be to skip the (array) cast in rest_preload_api_request(), either here or in #14671 since that PR covers preloaded requests.

Replace the `allow_fields`, `allow_embed` and `allow_links` options with
one `response_processing` route and handler option. It is `false` to
serve the response data as returned, or an array with `fields`, `embed`
and `links` keys to turn off single features: the `_fields` parameter
filtering the data, the `_embed` parameter embedding linked resources,
and the response links being added to the data as `_links`. Each
feature resolves on its own, the handler setting over the route
setting, and `embed` is off whenever `links` is off.

The settings are resolved once, when the request is matched, and are
read from the request attributes and the matched handler by the server,
by `rest_filter_response_fields()` and by
`WP_REST_Controller::get_fields_for_response()`. A response returned by
the `rest_pre_dispatch` filter follows the settings of the route it was
returned for. The index lists the settings of an endpoint that turns
off a feature, and `rest_preload_api_request()` no longer casts the
data to an array.

See #66267.
`rest_filter_response_fields()` leaves the response unchanged when its
data is not an array, and keeps list items that are not arrays.
`WP_REST_Server::response_to_data()` returns scalar and object data as
it is instead of adding links to it. Routes that return such data no
longer need the `response_processing` option to avoid type errors.

See #66267.
The run endpoint prepared only associative array results for JSON
clients, so an empty array returned for an object output schema, such as
the `core/settings-get` result when no setting matches, was encoded as []
and failed output validation in JSON clients. Prepare every result,
including root results, lists and object results, so empty objects are
encoded as {} while arrays stay arrays.

Disable the `_fields` and `_embed` parameters and response links for the
run endpoint, so the result follows the ability's output schema whether
it is an object, an array, or a scalar value.

Follow-up to [64311], [64592].

See #66267.
…g` route option.

Replace the `allow_fields`, `allow_embed` and `allow_links` route handler keys with `response_processing => false`, so `_fields`, `_embed` and response links leave the ability result unchanged, and regenerate the REST API client fixture, where the run endpoint now lists the disabled processing.

See #66267.
@jorgefilipecosta
jorgefilipecosta force-pushed the fix/abilities-empty-object-result branch from 374b904 to cd02d31 Compare October 22, 2026 14:40
@gziolo

gziolo commented Oct 23, 2026

Copy link
Copy Markdown
Member

I approved #14671, so let's commit it, and I will finish reviewing this one. Currently, it contains changes from both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants