[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> json-schema.php (source)

   1  <?php
   2  /**
   3   * JSON Schema API: shared functions for working with JSON Schema.
   4   *
   5   * @package WordPress
   6   * @subpackage JSON_Schema
   7   * @since 7.1.0
   8   */
   9  
  10  /**
  11   * Gets the JSON Schema keywords allowed for a given schema profile.
  12   *
  13   * Use this when preparing a schema that will be consumed outside of
  14   * WordPress's server-side validation, such as by REST clients, frontend code,
  15   * or AI providers.
  16   *
  17   * The 'rest-api' profile returns the subset of JSON Schema draft-04 keywords
  18   * that the REST API has historically exposed. The 'draft-04' profile preserves
  19   * the larger draft-04 vocabulary used by clients that can consume standalone
  20   * schemas.
  21   *
  22   * Allowing a keyword to be exposed does not make WordPress validate or
  23   * sanitize values against it.
  24   *
  25   * @since 7.1.0
  26   *
  27   * @param string $schema_profile Optional. Name of the schema profile to get keywords for.
  28   *                               Accepts 'rest-api' or 'draft-04'. Any other value falls
  29   *                               back to the 'rest-api' profile. Default 'rest-api'.
  30   * @return string[] Allowed JSON Schema keywords.
  31   */
  32  function wp_get_json_schema_allowed_keywords( string $schema_profile = 'rest-api' ): array {
  33      $rest_keywords = rest_get_allowed_schema_keywords();
  34  
  35      $keywords_by_profile = array(
  36          'rest-api' => $rest_keywords,
  37          'draft-04' => array_merge(
  38              array(
  39                  '$schema',
  40                  'id',
  41                  '$ref',
  42              ),
  43              $rest_keywords,
  44              array(
  45                  'required',
  46                  'allOf',
  47                  'not',
  48                  'definitions',
  49                  'dependencies',
  50                  'additionalItems',
  51              )
  52          ),
  53      );
  54  
  55      $allowed_keywords = $keywords_by_profile[ $schema_profile ] ?? $rest_keywords;
  56  
  57      /**
  58       * Filters the JSON Schema keywords allowed for a given schema profile.
  59       *
  60       * Use this to decide which keywords may be exposed to clients for a profile.
  61       * It does not make WordPress validate or sanitize values against the keyword.
  62       *
  63       * @since 7.1.0
  64       *
  65       * @param string[] $allowed_keywords Allowed JSON Schema keywords.
  66       * @param string   $schema_profile   The schema profile the keywords are for.
  67       */
  68      return apply_filters( 'wp_json_schema_allowed_keywords', $allowed_keywords, $schema_profile );
  69  }
  70  
  71  /**
  72   * Prepares a JSON Schema for clients.
  73   *
  74   * Converts WordPress-specific schema conventions and removes unsupported
  75   * keywords so clients can interpret the schema using the selected profile.
  76   *
  77   * Applies the same preparation to nested schemas. Existing default values
  78   * are normalized according to their schemas so empty objects encode as `{}`
  79   * rather than `[]`, including within nested default values.
  80   *
  81   * @since 7.1.0
  82   *
  83   * @param array<string, mixed> $schema         The schema array.
  84   * @param string               $schema_profile Optional. Name of the schema profile
  85   *                                             whose keywords should be preserved.
  86   *                                             Default 'draft-04'.
  87   * @return array<string, mixed> The prepared schema.
  88   */
  89  function wp_prepare_json_schema_for_client( array $schema, string $schema_profile = 'draft-04' ): array {
  90      $allowed_keywords = array_fill_keys( wp_get_json_schema_allowed_keywords( $schema_profile ), true );
  91  
  92      return _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema, $allowed_keywords );
  93  }
  94  
  95  /**
  96   * Prepares a JSON Schema for clients using a given keyword lookup.
  97   *
  98   * Normalizes schema conventions and default values throughout nested schemas
  99   * while retaining only the allowed keywords.
 100   *
 101   * @since 7.1.0
 102   * @access private
 103   *
 104   * @param array<string, mixed> $schema           The schema array.
 105   * @param array<string, true>  $allowed_keywords Lookup map of allowed JSON Schema keywords.
 106   * @return array<string, mixed> The prepared schema.
 107   */
 108  function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema, array $allowed_keywords ): array {
 109      if ( array_key_exists( 'default', $schema ) ) {
 110          $schema['default'] = wp_prepare_json_value_for_client( $schema['default'], $schema );
 111      }
 112  
 113      $schema = array_intersect_key( $schema, $allowed_keywords );
 114  
 115      /*
 116       * Collect draft-03 per-property `required: true` flags into a draft-04
 117       * `required` array of property names on the parent object schema.
 118       *
 119       * This mirrors rest_validate_object_value_from_schema(), where a draft-04
 120       * `required` array takes precedence: when one is present, per-property
 121       * booleans are ignored during validation. They are therefore left out of
 122       * the array here as well (but still stripped from the output) so the
 123       * published schema describes exactly what gets enforced.
 124       */
 125      if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
 126          $has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
 127          $required           = array();
 128          foreach ( $schema['properties'] as $property => &$property_schema ) {
 129              if ( is_array( $property_schema ) && ! wp_is_numeric_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
 130                  if ( ! $has_required_array && true === $property_schema['required'] ) {
 131                      $required[] = (string) $property;
 132                  }
 133                  unset( $property_schema['required'] );
 134              }
 135          }
 136          unset( $property_schema );
 137  
 138          /*
 139           * Property keys are unique, so the collected list needs no deduplication.
 140           * When a draft-04 array is already present, leave it untouched.
 141           */
 142          if ( ! $has_required_array && count( $required ) > 0 ) {
 143              $schema['required'] = $required;
 144          }
 145      }
 146  
 147      /*
 148       * A boolean `required` outside of an object's property list has no draft-04
 149       * equivalent, so drop it rather than emit an invalid keyword.
 150       */
 151      if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
 152          unset( $schema['required'] );
 153      }
 154  
 155      /*
 156       * Sub-schema maps: keys are user-defined, values are sub-schemas.
 157       * Note: 'dependencies' values can also be property-dependency arrays
 158       * (numeric arrays of strings) which are skipped via wp_is_numeric_array().
 159       */
 160      foreach ( array( 'properties', 'patternProperties', 'definitions', 'dependencies' ) as $keyword ) {
 161          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
 162              foreach ( $schema[ $keyword ] as $key => $child_schema ) {
 163                  if ( is_array( $child_schema ) && ! wp_is_numeric_array( $child_schema ) ) {
 164                      $schema[ $keyword ][ $key ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $child_schema, $allowed_keywords );
 165                  }
 166              }
 167          }
 168      }
 169  
 170      // Single sub-schema keywords.
 171      foreach ( array( 'not', 'additionalProperties', 'additionalItems' ) as $keyword ) {
 172          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) && ! wp_is_numeric_array( $schema[ $keyword ] ) ) {
 173              $schema[ $keyword ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema[ $keyword ], $allowed_keywords );
 174          }
 175      }
 176  
 177      // Items: single schema or tuple array of schemas.
 178      if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
 179          if ( ! wp_is_numeric_array( $schema['items'] ) ) {
 180              $schema['items'] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema['items'], $allowed_keywords );
 181          } else {
 182              foreach ( $schema['items'] as $index => $item_schema ) {
 183                  if ( is_array( $item_schema ) && ! wp_is_numeric_array( $item_schema ) ) {
 184                      $schema['items'][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $item_schema, $allowed_keywords );
 185                  }
 186              }
 187          }
 188      }
 189  
 190      // Array-of-schemas keywords.
 191      foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $keyword ) {
 192          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
 193              foreach ( $schema[ $keyword ] as $index => $sub_schema ) {
 194                  if ( is_array( $sub_schema ) && ! wp_is_numeric_array( $sub_schema ) ) {
 195                      $schema[ $keyword ][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $sub_schema, $allowed_keywords );
 196                  }
 197              }
 198          }
 199      }
 200  
 201      return $schema;
 202  }
 203  
 204  /**
 205   * Prepares a value for JSON encoding according to its schema.
 206   *
 207   * Preserves the distinction between JSON objects and arrays by converting
 208   * empty PHP arrays to objects where the schema identifies them as objects.
 209   * Nested properties and items are prepared according to their schemas.
 210   * Empty arrays remain arrays when a matching schema allows them.
 211   *
 212   * Prepares the representation of existing values without inserting defaults
 213   * or performing full JSON Schema validation.
 214   *
 215   * @since 7.2.0
 216   *
 217   * @param mixed                $value  The value to prepare.
 218   * @param array<string, mixed> $schema The schema describing the value.
 219   * @return mixed The value prepared for JSON encoding.
 220   */
 221  function wp_prepare_json_value_for_client( $value, array $schema ) {
 222      if ( $value instanceof JsonSerializable ) {
 223          $value = $value->jsonSerialize();
 224      }
 225  
 226      if ( ! is_array( $value ) && ! $value instanceof stdClass ) {
 227          return $value;
 228      }
 229  
 230      foreach ( array( 'anyOf', 'oneOf' ) as $keyword ) {
 231          if ( ! isset( $schema[ $keyword ] ) ) {
 232              continue;
 233          }
 234  
 235          $matching_schema = null;
 236  
 237          /*
 238           * Prefer an array schema for PHP lists. An empty PHP array also matches an
 239           * object schema, but should remain [] when an array schema accepts it.
 240           */
 241          if ( is_array( $value ) && array_is_list( $value ) ) {
 242              foreach ( $schema[ $keyword ] as $candidate_schema ) {
 243                  if ( ! isset( $candidate_schema['type'] ) && isset( $schema['type'] ) ) {
 244                      $candidate_schema['type'] = $schema['type'];
 245                  }
 246  
 247                  if ( _wp_json_schema_matches_type( $value, $candidate_schema, 'array' ) ) {
 248                      $matching_schema = $candidate_schema;
 249                      break;
 250                  }
 251              }
 252          }
 253  
 254          // Like rest_filter_response_by_context(), use the first matching oneOf branch.
 255          if ( null === $matching_schema ) {
 256              $matching_schema = 'anyOf' === $keyword
 257                  ? rest_find_any_matching_schema( $value, $schema, '' )
 258                  : rest_find_one_matching_schema( $value, $schema, '', true );
 259          }
 260  
 261          if ( is_wp_error( $matching_schema ) ) {
 262              continue;
 263          }
 264  
 265          if ( ! isset( $schema['type'] ) && isset( $matching_schema['type'] ) ) {
 266              $schema['type'] = $matching_schema['type'];
 267          }
 268  
 269          $value = wp_prepare_json_value_for_client( $value, $matching_schema );
 270      }
 271  
 272      $is_array_value = is_array( $value );
 273      $types          = isset( $schema['type'] ) ? (array) $schema['type'] : array();
 274      $allows_object  = in_array( 'object', $types, true );
 275      $allows_array   = in_array( 'array', $types, true );
 276  
 277      if ( $is_array_value && empty( $value ) && $allows_object && ! $allows_array ) {
 278          return (object) $value;
 279      }
 280  
 281      $is_object_value = $value instanceof stdClass || ( $is_array_value && $allows_object && ( ! $allows_array || ! array_is_list( $value ) ) );
 282      if ( $allows_object && $is_object_value ) {
 283          if ( $value instanceof stdClass ) {
 284              $value = clone $value;
 285          }
 286  
 287          foreach ( $value as $property => $property_value ) {
 288              $property_schema = null;
 289              if ( isset( $schema['properties'][ $property ] ) && is_array( $schema['properties'][ $property ] ) ) {
 290                  $property_schema = $schema['properties'][ $property ];
 291              } else {
 292                  $property_schema = rest_find_matching_pattern_property_schema( $property, $schema );
 293                  if ( null === $property_schema && isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
 294                      $property_schema = $schema['additionalProperties'];
 295                  }
 296              }
 297  
 298              if ( null === $property_schema ) {
 299                  continue;
 300              }
 301  
 302              $property_value = wp_prepare_json_value_for_client( $property_value, $property_schema );
 303              if ( is_array( $value ) ) {
 304                  $value[ $property ] = $property_value;
 305              } else {
 306                  $value->$property = $property_value;
 307              }
 308          }
 309      } elseif ( $allows_array && $is_array_value && isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
 310          foreach ( $value as $index => $item ) {
 311              $value[ $index ] = wp_prepare_json_value_for_client( $item, $schema['items'] );
 312          }
 313      }
 314  
 315      return $value;
 316  }
 317  
 318  /**
 319   * Determines whether a value matches a JSON Schema through a given type.
 320   *
 321   * Combining schemas must have a matching branch that allows the requested type.
 322   * Finding the type in one branch and validating the value against another branch
 323   * is not sufficient.
 324   *
 325   * @since 7.2.0
 326   * @access private
 327   *
 328   * @param mixed                $value  The value to validate.
 329   * @param array<string, mixed> $schema The schema to inspect.
 330   * @param string               $type   The type to match.
 331   * @return bool Whether the value matches the schema through the requested type.
 332   */
 333  function _wp_json_schema_matches_type( $value, array $schema, string $type ): bool {
 334      if ( isset( $schema['type'] ) && ! in_array( $type, (array) $schema['type'], true ) ) {
 335          return false;
 336      }
 337  
 338      foreach ( array( 'anyOf', 'oneOf' ) as $keyword ) {
 339          if ( ! isset( $schema[ $keyword ] ) ) {
 340              continue;
 341          }
 342  
 343          foreach ( $schema[ $keyword ] as $sub_schema ) {
 344              if ( ! is_array( $sub_schema ) ) {
 345                  continue;
 346              }
 347  
 348              // Match rest_find_any_matching_schema(), which applies the parent type to untyped branches.
 349              if ( ! isset( $sub_schema['type'] ) && isset( $schema['type'] ) ) {
 350                  $sub_schema['type'] = $schema['type'];
 351              }
 352  
 353              if ( _wp_json_schema_matches_type( $value, $sub_schema, $type ) ) {
 354                  continue 2;
 355              }
 356          }
 357  
 358          return false;
 359      }
 360  
 361      if ( ! isset( $schema['type'] ) && ! isset( $schema['anyOf'] ) && ! isset( $schema['oneOf'] ) ) {
 362          return false;
 363      }
 364  
 365      // Explicit and inferred types must both validate through the requested type only.
 366      $schema['type'] = $type;
 367  
 368      return ! is_wp_error( rest_validate_value_from_schema( $value, $schema ) );
 369  }


Generated : Sat Oct 24 08:20:34 2026 Cross-referenced by PHPXref