Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,20 @@ private function is_associative_array( $value ): bool {
/**
* Transforms an ability schema for REST response output.
*
* The input and output schemas are a public contract: REST clients (such as
* the `@wordpress/abilities` JS client) consume them as standard JSON Schema
* and validate ability input and output against them. The response must
* therefore use JSON Schema draft-04 forms that standard validators
* understand, not the WordPress-internal conventions that
* `rest_validate_value_from_schema()` also accepts on the server.
*
* Ability schemas may include WordPress-internal properties or unsupported
* schema keywords that should not be exposed in REST responses. This method
* strips keys not recognized by the REST API schema handling. It also
* converts empty array defaults to objects when the schema type is 'object'
* to ensure proper JSON serialization as {} instead of [].
* to ensure proper JSON serialization as {} instead of [], and normalizes
* the `required` keyword from the draft-03 per-property boolean form into
* the draft-04 array of property names.
*
* @since 7.1.0
*
Expand All @@ -256,6 +265,40 @@ private function prepare_schema_for_response( array $schema ): array {

$schema = array_intersect_key( $schema, $allowed_keywords );

// Collect draft-03 per-property `required: true` flags into a draft-04
// `required` array of property names on the parent object schema.
//
// This mirrors rest_validate_object_value_from_schema(), where a draft-04
// `required` array takes precedence: when one is present, per-property
// booleans are ignored during validation. They are therefore left out of
// the array here as well (but still stripped from the output) so the
// published schema describes exactly what gets enforced.
if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
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.

I wonder if REST is the right place for this transformation or if it is something that should be considered at the ability registration level. The advantage of being on the ability level, is that for things like wp_ai_client use abilities and MCP adapter the abilities would also have the draft-04 shape.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's an open question to me as well. I shared more thoughts in https://core.trac.wordpress.org/ticket/64955 on the topic. I decided to start with an actual use case, which solves a real problem that developers might encounter when using the ability registered on the server through the newly introduced @wordpress/abilities package.

If you look at steps 5 and 6 outlined in https://core.trac.wordpress.org/ticket/64955#comment:1, I covered two possible pathways:

  • Step 5: Emit authoring guidance via _doing_it_wrong()
  • Step 6: Introduce schema_version for opt-in strict authoring

The original idea I had was that we eventually tap into the ability registration phase and first warn about schema incompatibilities for existing abilities while auto-correcting them (like in REST API now), and offer a way to enable strict validation that would prevent registration when the schema doesn't follow the spec.

If we are on board with that plan, then we can follow up with these strategies in separate PRs.

$has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
$required = array();
foreach ( $schema['properties'] as $property => &$property_schema ) {
if ( $this->is_associative_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
if ( ! $has_required_array && true === $property_schema['required'] ) {
$required[] = (string) $property;
}
unset( $property_schema['required'] );
}
}
unset( $property_schema );

// Property keys are unique, so the collected list needs no deduplication.
// When a draft-04 array is already present, leave it untouched.
if ( ! $has_required_array && count( $required ) > 0 ) {
$schema['required'] = $required;
}
}

// A boolean `required` outside of an object's property list has no draft-04
// equivalent, so drop it rather than emit an invalid keyword.
if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
unset( $schema['required'] );
}

// Sub-schema maps: keys are user-defined, values are sub-schemas.
// Note: 'dependencies' values can also be property-dependency arrays
// (numeric arrays of strings) which are skipped via wp_is_numeric_array().
Expand Down
25 changes: 25 additions & 0 deletions tests/phpunit/tests/rest-api/rest-schema-validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,31 @@ public function data_required_deeply_nested_property() {
);
}

/**
* A draft-04 `required` array takes precedence over per-property
* `required` booleans on the same object node: the booleans are ignored.
*
* @ticket 64955
*/
public function test_required_v4_array_takes_precedence_over_v3_booleans() {
$schema = array(
'type' => 'object',
'required' => array( 'listed' ),
'properties' => array(
'listed' => array( 'type' => 'string' ),
'flagged' => array(
'type' => 'string',
'required' => true, // Ignored because the array is present.
),
),
);

// Missing the array-listed prop fails.
$this->assertWPError( rest_validate_value_from_schema( array( 'flagged' => 'x' ), $schema ) );
// Missing only the boolean-flagged prop passes — the boolean is not enforced.
$this->assertTrue( rest_validate_value_from_schema( array( 'listed' => 'x' ), $schema ) );
}

/**
* @ticket 51023
*/
Expand Down
Loading
Loading