Skip to content

Conversation

@bordoni
Copy link
Member

@bordoni bordoni commented Oct 11, 2025

This PR adds serialization methods to both WP_Ability and WP_Ability_Category classes to provide declarative ways to export data for use in REST APIs, JavaScript contexts, and schema validation.

Changes

WP_Ability Methods

WP_Ability::to_array()

Returns a complete array representation of the ability including:

  • name - The ability name with namespace
  • label - Human-readable label
  • description - Detailed description
  • input_schema - Input validation schema (if defined)
  • output_schema - Output validation schema (if defined)
  • meta - Metadata including annotations and show_in_rest

Callbacks (execute_callback, permission_callback) are excluded as they are not serializable.

Use cases:

  • REST API responses
  • Declarative ability export
  • JavaScript consumption
  • Debugging and inspection

WP_Ability::to_json_schema()

Returns a JSON Schema Draft 4 compliant schema describing the ability's structure. The schema includes:

  • Ability metadata (title, description)
  • Input/output schema definitions
  • Meta structure with annotations and REST visibility
  • Uses enum array for the ability name (Draft 4 compatible)

Note: Uses JSON Schema Draft 4 to maintain compatibility with WordPress REST API standards.

Use cases:

  • JavaScript validation
  • API documentation generation
  • Schema introspection
  • Type generation for TypeScript/Flow

WP_Ability::jsonSerialize()

Implements the JsonSerializable interface, allowing abilities to be passed directly to json_encode().

WP_Ability_Category Methods

WP_Ability_Category::to_array()

Returns a complete array representation of the category including:

  • slug - The unique category slug
  • label - Human-readable label
  • description - Detailed description
  • meta - Optional metadata

Use cases:

  • REST API responses
  • Category enumeration
  • JavaScript consumption

WP_Ability_Category::jsonSerialize()

Implements the JsonSerializable interface for direct JSON encoding support.

Filters

Both classes include filters to allow developers to modify the output:

WP_Ability Filters:

  • wp_ability_{name}_to_array - Filters the array representation

    • @param array $array - The ability as an associative array
    • @param \WP_Ability $ability - The ability instance
  • wp_ability_{name}_to_json_schema - Filters the JSON Schema representation

    • @param array $schema - The JSON Schema representation
    • @param \WP_Ability $ability - The ability instance

WP_Ability_Category Filters:

  • wp_ability_category_{slug}_to_array - Filters the category array representation
    • @param array $array - The category as an associative array
    • @param \WP_Ability_Category $category - The category instance

Test Coverage

Added comprehensive test coverage:

  • 15 tests for WP_Ability serialization (to_array, to_json_schema, JsonSerializable)
  • 9 tests for WP_Ability_Category serialization (to_array, JsonSerializable)
  • Total: 24 new tests covering:
    • Structure validation
    • Schema validation (Draft 4 compliance)
    • Filter functionality and parameter passing
    • JSON encoding via JsonSerializable
    • Meta structure handling
    • Edge cases and nested data

Implementation Details

  • Uses public getter methods instead of direct property access for proper encapsulation
  • Implements JsonSerializable interface on both classes
  • Maintains backward compatibility
  • Keeps annotations and show_in_rest under meta structure as per existing conventions
  • JSON Schema uses Draft 4 for WordPress REST API compatibility

Example Usage

// WP_Ability usage $ability = wp_get_ability( 'core/get-bloginfo' ); // Get array representation $array = $ability->to_array(); // Get JSON Schema $schema = $ability->to_json_schema(); // Direct JSON encoding $json = json_encode( $ability ); // Filter example add_filter( 'wp_ability_core/get-bloginfo_to_array', function( $array, $ability ) { $array['custom_field'] = 'custom_value'; return $array; }, 10, 2 ); // WP_Ability_Category usage $category = wp_get_ability_category( 'math' ); // Get array representation $cat_array = $category->to_array(); // Direct JSON encoding $cat_json = json_encode( $category ); // Filter example add_filter( 'wp_ability_category_math_to_array', function( $array, $category ) { $array['icon'] = 'dashicons-calculator'; return $array; }, 10, 2 );

Addresses

Implements feedback from @JasonTheAdams in PR #108

- Add WP_Ability::to_array() method for declarative array export - Add WP_Ability::to_json_schema() method for JSON Schema generation - Add comprehensive test coverage for both methods - Ensure annotations and show_in_rest remain under meta structure The to_array() method returns a complete array representation of the ability including name, label, description, schemas, and metadata, excluding callbacks as they are not serializable. The to_json_schema() method generates a JSON Schema Draft 7 compliant schema describing the ability's structure, useful for validation in both PHP and JavaScript contexts. Implements feedback from PR WordPress#108.
Replace direct property access with public getter methods in to_array() and to_json_schema() methods to follow encapsulation best practices.
Add wp_ability_{name}_to_array and wp_ability_{name}_to_json_schema filters to allow developers to modify the output of both methods. - Add filter documentation with @SInCE tags - Pass ability instance as second parameter to filters - Add comprehensive test coverage for filter functionality - Verify filters receive correct parameters and modify output
@github-actions
Copy link

github-actions bot commented Oct 11, 2025

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: bordoni <bordoni@git.wordpress.org> Co-authored-by: westonruter <westonruter@git.wordpress.org> Co-authored-by: gziolo <gziolo@git.wordpress.org> Co-authored-by: justlevine <justlevine@git.wordpress.org> Co-authored-by: felixarntz <flixos90@git.wordpress.org> Co-authored-by: mindctrl <mindctrl@git.wordpress.org> Co-authored-by: JasonTheAdams <jason_the_adams@git.wordpress.org> 

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

@codecov
Copy link

codecov bot commented Oct 11, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.61%. Comparing base (0a1cda6) to head (63e3b5f).

Additional details and impacted files
@@ Coverage Diff @@ ## trunk #109 +/- ## ============================================= + Coverage 80.46% 94.61% +14.14%  ============================================= Files 20 7 -13 Lines 1474 334 -1140 Branches 119 119 ============================================= - Hits 1186 316 -870  + Misses 288 18 -270 
Flag Coverage Δ
javascript 94.61% <ø> (ø)
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
Add JsonSerializable interface to allow abilities to be passed directly to json_encode() without manually calling to_array(). - Implement jsonSerialize() method that delegates to to_array() - Automatically applies wp_ability_{name}_to_array filter - Add 4 comprehensive tests for JsonSerializable functionality - Verify json_encode() works correctly with WP_Ability instances Addresses feedback from @westonruter in PR WordPress#109.
@bordoni bordoni requested a review from westonruter October 11, 2025 23:32
Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

Thank you for opening this proposal.

I see some value in introducing ::to_array. I left some feedback regarding the proposed implementation. I wanted to note that the description of this PR mentions applicability to REST API, so I was wondering why the REST API controller wasn't updated to use the new API:

$data = array(
'name' => $ability->get_name(),
'label' => $ability->get_label(),
'description' => $ability->get_description(),
'input_schema' => $ability->get_input_schema(),
'output_schema' => $ability->get_output_schema(),
'meta' => $ability->get_meta(),
);

Aside: I see that we still need to account for non-serializable data in meta in the exisitng REST API implementaiton, too.

Regarding the to_json_schema proposal. I'm a bit more skeptical it's useful on the WP_Ability class level. There is an existing schema in place in the REST API endpoint:

public function get_item_schema(): array {
$schema = array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'ability',
'type' => 'object',
'properties' => array(
'name' => array(
'description' => __( 'Unique identifier for the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'label' => array(
'description' => __( 'Display label for the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'description' => array(
'description' => __( 'Description of the ability.' ),
'type' => 'string',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'input_schema' => array(
'description' => __( 'JSON Schema for the ability input.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'output_schema' => array(
'description' => __( 'JSON Schema for the ability output.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'annotations' => array(
'description' => __( 'Annotations for the ability.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'meta' => array(
'description' => __( 'Meta information about the ability.' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
),
'required' => array( 'name', 'label', 'description' ),
);
return $this->add_additional_fields_schema( $schema );

WordPress is using its flavor of JSON Schema Draft 4 with some non-standard properties:

https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/#json-schema-basics

What would be the practical usage of JSON Schema draft 7 included on the WP_Ability level? I don't see it applicable inside WordPress core given the explanation above.

'description' => $this->get_description(),
'input_schema' => $this->get_input_schema(),
'output_schema' => $this->get_output_schema(),
'meta' => $this->get_meta(),
Copy link
Member

Choose a reason for hiding this comment

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

Here, to make the PHPDoc correct, you would have to account for the callback in any custom meta provided or limit that object to annotations and show_in_rest.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point I will change the docblock, as I feel like the to_array() should allow other meta.

* @param array<string,mixed> $array The ability as an associative array.
* @param \WP_Ability $ability The ability instance.
*/
return apply_filters( "wp_ability_{$this->get_name()}_to_array", $array, $this );
Copy link
Member

Choose a reason for hiding this comment

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

Some example from WP core:

I don't see filters used there. @justlevine shared some concerns regarding using filters on methods that are executed multiple times in #37 (comment). More broadly, I'm curious what scenarios this helper method would require these filters, taking into account that the consumer can still manipulate the result however they like.

Copy link
Member Author

Choose a reason for hiding this comment

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

Filtering here allows individual modification of the "context" for the AI models later on, which is not required for the other methods like this one

Copy link
Member

Choose a reason for hiding this comment

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

Can you share an example of how this ::to_array() would be consumed to pass to the AI model? It isn't used this way in the codebase currently, so I might be missing some nuance here.

Copy link
Contributor

@justlevine justlevine Oct 13, 2025

Choose a reason for hiding this comment

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

@gziolo thanks for tagging me 🙇

IMO semantically it would make sense that a filter on to_array() would trigger every time a WP_Ability is transformed, so I don't have that same concern here.

I also like @bordoni 's use case, since that sort of tailoring wouldn't really make sense extending the class to overload this function. (Though would like to know more as to what modifications you would want injected at that point in the lifecycle and not earlier).

That said, I do think it's perfectly fine for WordPress 6.9 to not allow devs to inject top-level properties via a hook. They can already filter args, and dump whatever they want in meta, so another filter feels like something we can do additively in 7.0 and later.

(Per previous decisions we can merge now and decide whether to strip it during beta).

Copy link
Member

@felixarntz felixarntz Oct 14, 2025

Choose a reason for hiding this comment

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

When wanting to keep structures concrete, filters are not a great tool :)

It's a problem that's discussed over and over, every once in a while it comes up, but obviously, in a filter callback one can do anything one likes. I am not opposed to adding a filter here, but I am much more of a proponent of adding one based on concrete needs.

In WordPress Core, every API interface you add will have to be there more or less forever. You can't remove it. But you can always add something. So unless there's a critical need we know of ahead of time, I'd always vote for most minimal public API surfaces possible.

If we still want to proceed with adding a filter here right away, I'd strongly suggest to not have it run on the entire data. The use-case mentioned is to inject additional key-value pairs - that's totally reasonable and can be done by filtering an empty array, then merging it into the base result. This way, we allow adding stuff, but we don't allow messing up the base result, which is critical to ensure data integrity.

Alternatively, to achieve the same goal, create a copy of $array first, then filter the copy (this way the filter retains access to the whole associative array and can operate on it), and after the filter application merge the original array back into the copy (to ensure the original data is still the same). The filter docs would then need to mention that this can only be used to add additional keys, not to change existing keys.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there already an existing use case for the filters, or is this a case of "we expect people will want this"? It's not a popular opinion in the WP community, but I'm a big fan of not adding filters to early versions of things that don't have a lot of use yet. It's early days and we may need to change things we can't realize yet. In other words, I'm a fan of code that's easy to change, at this stage of development.

Like @felixarntz says above, every API interface you add basically has to remain forever, unless/until it's deprecated and replaced by something else. In this case, we have PHP classes, methods, and filters, all of which are public and a form of an API, and all of which are harder to change once they're in the wild.

If there are already solid needs for the filters, then the additive approach mentioned by Felix seems the better way to go.

@gziolo gziolo added the [Type] Enhancement New feature or request label Oct 13, 2025
@bordoni
Copy link
Member Author

bordoni commented Oct 13, 2025

@gziolo I can adjust to use the JSON Schema Draft 4, my goal with this PR was to address the idea that you could look and potentially modify the WP_Ability in a unified place.

I can decouple the ::to_json_schema() from this PR if you all have strong opinions on the value of that method.

@gziolo
Copy link
Member

gziolo commented Oct 13, 2025

The challenge is that if we even update ::to_json_schema to use it with REST API this has implications as it must remain backward compatible for all WP versions. That's why I asked to provide some specific examples where you see application for that method. To give an example, OpenAI API supports structured model outputs, but it currently supports only a subset of functionality that WP REST API schema does, and some parts of the API are custom in the WP space.

@JasonTheAdams
Copy link
Member

Thanks so much for this, @bordoni!

Since this is based off my comment, I'll elaborate on my intent with this: We know that abilities will be represented both in memory (PHP) as well as in JSON — input schema, output schemas (e.g. REST), and for use in JS. Having a consolidated way for this to be represented in each context is useful.

We actually did this with all the many DTOs in the PHP AI client. We created two Interfaces every DTO has:

We also used @phpstan-type to define and import types throughout for strong typing. The schema is also used in the REST schemas in the WP AI Client.

With that in mind, I felt like it could be a good use case for Abilities and Ability Categories (#102), since we know it will be used in a number of contexts, and keeping that all in sync could be a bear.

Finally, with regards to v4 vs v7 JSON schema, my inclination is to lean on v4 since REST relies on it (😭), but if v7 is necessary, then I'd make a translation layer for REST (even if it's just a get_v4_json_schema() method). We should be moving more of WP towards v7, in my opinion, not completely digging ourselves into v4.

 - Clarified that $meta array may contain additional custom keys beyond documented ones - Added @type mixed ...$0 parameter to document extensibility of meta array - Fixed alignment of @type bool $show_in_rest for consistency
@JasonTheAdams
Copy link
Member

The challenge is that if we even update ::to_json_schema to use it with REST API this has implications as it must remain backward compatible for all WP versions. That's why I asked to provide some specific examples where you see application for that method. To give an example, OpenAI API supports structured model outputs, but it currently supports only a subset of functionality that WP REST API schema does, and some parts of the API are custom in the WP space.

You're definitely right, here. The schema support in the REST API is... not ideal. Hahah! I'm really wondering if having a to_rest_schema() could be useful, as we could even check the WP version if what the schema supports eventually changes. That said, it may not be too big of a deal in this exact case if the schema is simple enough.

I also wouldn't generically have the REST API pull this in, so the implementing code can also play a role in making this work properly.

- Implement JsonSerializable interface for WP_Ability_Category - Add to_array() method returning slug, label, description, and meta - Add wp_ability_category_{slug}_to_array filter - Add 9 comprehensive tests covering array conversion and JSON serialization Maintains consistency with WP_Ability implementation pattern.
- Change schema version from Draft 7 to Draft 4 for WP REST API compatibility - Replace `const` keyword with `enum` array (Draft 4 doesn't support const) - Update test assertions to check for enum instead of const - Update docblock to reflect Draft 4 compliance All 100 tests passing with new schema format.
@bordoni
Copy link
Member Author

bordoni commented Oct 13, 2025

@gziolo @JasonTheAdams Modified to use Draft 4 to match the WordPress default, we can talk about others later on. I also got the WP_Ability_Category a ->to_array() and serialization process.

@bordoni bordoni changed the title Add to_array() and to_json_schema() methods to WP_Ability Add serialization methods to WP_Ability and WP_Ability_Category Oct 13, 2025
@felixarntz
Copy link
Member

I'm curious regarding JSON Schema v4 vs v7, what are the concrete differences that affect us most in WordPress? To my knowledge (not being very familiar with the spec), an important distinction is having a required bool value on each object property vs a required list value with property names in it on the overall object. If it's mostly just that, we could go either way here (probably v4, since this is WordPress) and have a simple translation layer to the respective other format.

While I would like to move WordPress to v7, I think this is not a realistic goal in anywhere near-term. Backward compatibility is crucial, and more or less every REST API endpoint schema and arguments definition would need to be modified if we made the switch. So I wouldn't bet on that anytime soon, we need to align with WordPress's status quo while making adaption with other paradigms easy.

@JasonTheAdams
Copy link
Member

I'm curious regarding JSON Schema v4 vs v7, what are the concrete differences that affect us most in WordPress?

The main quirk that always surprises me in the REST schema is that each property is evaluated on its own during validation. That is, when validating a given property within the schema, only that property's part of the schema is inspected, not the whole schema object. This means that you cannot reference multiple properties at once using something like a oneOf. It's a really frustrating limitation. This is in part why required is part of the property definition and not the typical array in the schema root.

@gziolo
Copy link
Member

gziolo commented Oct 15, 2025

As we are getting feedback sorted out, I'd like to better understand whether the intention was also to refactor existing usage in the codebase, or if this is purely for the convenience of extenders? I shared code references in #109 (review).

@bordoni
Copy link
Member Author

bordoni commented Oct 15, 2025

I would be more than glad to setup a secondary PR or do it here the refactor of the existing stuff to use this new structure. Just wanted to get approval on if this part looks good. (huge PRs can be such a pain to review)

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

Labels

[Type] Enhancement New feature or request

7 participants