[ 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   * Use this before exposing a schema outside of WordPress's server-side
  75   * validation, for example in REST responses, Ability metadata, or AI provider
  76   * requests. The prepared schema uses forms that JSON Schema draft-04 clients
  77   * can understand.
  78   *
  79   * WordPress-internal schema conveniences are converted or removed only where
  80   * needed to keep the exposed schema valid for the selected profile.
  81   *
  82   * @since 7.1.0
  83   *
  84   * @param array<string, mixed> $schema         The schema array.
  85   * @param string               $schema_profile Optional. Name of the schema profile
  86   *                                             whose keywords should be preserved.
  87   *                                             Default 'draft-04'.
  88   * @return array<string, mixed> The prepared schema.
  89   */
  90  function wp_prepare_json_schema_for_client( array $schema, string $schema_profile = 'draft-04' ): array {
  91      $allowed_keywords = array_fill_keys( wp_get_json_schema_allowed_keywords( $schema_profile ), true );
  92  
  93      return _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema, $allowed_keywords );
  94  }
  95  
  96  /**
  97   * Prepares a JSON Schema for clients using a given keyword lookup.
  98   *
  99   * @since 7.1.0
 100   * @access private
 101   *
 102   * @param array<string, mixed> $schema           The schema array.
 103   * @param array<string, true>  $allowed_keywords Lookup map of allowed JSON Schema keywords.
 104   * @return array<string, mixed> The prepared schema.
 105   */
 106  function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema, array $allowed_keywords ): array {
 107      if ( isset( $schema['type'] ) && 'object' === $schema['type'] && isset( $schema['default'] ) ) {
 108          $default = $schema['default'];
 109          if ( is_array( $default ) && empty( $default ) ) {
 110              $schema['default'] = (object) $default;
 111          }
 112      }
 113  
 114      $schema = array_intersect_key( $schema, $allowed_keywords );
 115  
 116      /*
 117       * Collect draft-03 per-property `required: true` flags into a draft-04
 118       * `required` array of property names on the parent object schema.
 119       *
 120       * This mirrors rest_validate_object_value_from_schema(), where a draft-04
 121       * `required` array takes precedence: when one is present, per-property
 122       * booleans are ignored during validation. They are therefore left out of
 123       * the array here as well (but still stripped from the output) so the
 124       * published schema describes exactly what gets enforced.
 125       */
 126      if ( isset( $schema['properties'] ) && is_array( $schema['properties'] ) ) {
 127          $has_required_array = isset( $schema['required'] ) && is_array( $schema['required'] );
 128          $required           = array();
 129          foreach ( $schema['properties'] as $property => &$property_schema ) {
 130              if ( is_array( $property_schema ) && ! wp_is_numeric_array( $property_schema ) && isset( $property_schema['required'] ) && is_bool( $property_schema['required'] ) ) {
 131                  if ( ! $has_required_array && true === $property_schema['required'] ) {
 132                      $required[] = (string) $property;
 133                  }
 134                  unset( $property_schema['required'] );
 135              }
 136          }
 137          unset( $property_schema );
 138  
 139          /*
 140           * Property keys are unique, so the collected list needs no deduplication.
 141           * When a draft-04 array is already present, leave it untouched.
 142           */
 143          if ( ! $has_required_array && count( $required ) > 0 ) {
 144              $schema['required'] = $required;
 145          }
 146      }
 147  
 148      /*
 149       * A boolean `required` outside of an object's property list has no draft-04
 150       * equivalent, so drop it rather than emit an invalid keyword.
 151       */
 152      if ( isset( $schema['required'] ) && is_bool( $schema['required'] ) ) {
 153          unset( $schema['required'] );
 154      }
 155  
 156      /*
 157       * Sub-schema maps: keys are user-defined, values are sub-schemas.
 158       * Note: 'dependencies' values can also be property-dependency arrays
 159       * (numeric arrays of strings) which are skipped via wp_is_numeric_array().
 160       */
 161      foreach ( array( 'properties', 'patternProperties', 'definitions', 'dependencies' ) as $keyword ) {
 162          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
 163              foreach ( $schema[ $keyword ] as $key => $child_schema ) {
 164                  if ( is_array( $child_schema ) && ! wp_is_numeric_array( $child_schema ) ) {
 165                      $schema[ $keyword ][ $key ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $child_schema, $allowed_keywords );
 166                  }
 167              }
 168          }
 169      }
 170  
 171      // Single sub-schema keywords.
 172      foreach ( array( 'not', 'additionalProperties', 'additionalItems' ) as $keyword ) {
 173          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) && ! wp_is_numeric_array( $schema[ $keyword ] ) ) {
 174              $schema[ $keyword ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema[ $keyword ], $allowed_keywords );
 175          }
 176      }
 177  
 178      // Items: single schema or tuple array of schemas.
 179      if ( isset( $schema['items'] ) && is_array( $schema['items'] ) ) {
 180          if ( ! wp_is_numeric_array( $schema['items'] ) ) {
 181              $schema['items'] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $schema['items'], $allowed_keywords );
 182          } else {
 183              foreach ( $schema['items'] as $index => $item_schema ) {
 184                  if ( is_array( $item_schema ) && ! wp_is_numeric_array( $item_schema ) ) {
 185                      $schema['items'][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $item_schema, $allowed_keywords );
 186                  }
 187              }
 188          }
 189      }
 190  
 191      // Array-of-schemas keywords.
 192      foreach ( array( 'anyOf', 'oneOf', 'allOf' ) as $keyword ) {
 193          if ( isset( $schema[ $keyword ] ) && is_array( $schema[ $keyword ] ) ) {
 194              foreach ( $schema[ $keyword ] as $index => $sub_schema ) {
 195                  if ( is_array( $sub_schema ) && ! wp_is_numeric_array( $sub_schema ) ) {
 196                      $schema[ $keyword ][ $index ] = _wp_prepare_json_schema_for_client_with_allowed_keywords( $sub_schema, $allowed_keywords );
 197                  }
 198              }
 199          }
 200      }
 201  
 202      return $schema;
 203  }


Generated : Tue Jul 14 08:20:16 2026 Cross-referenced by PHPXref