@@ -93,6 +93,116 @@ function wp_prepare_json_schema_for_client( array $schema, string $schema_profil
9393 return _wp_prepare_json_schema_for_client_with_allowed_keywords ( $ schema , $ allowed_keywords );
9494}
9595
96+ /**
97+ * Prepares a JSON value for clients according to its schema.
98+ *
99+ * Empty PHP arrays are converted to objects when the schema allows objects but
100+ * not arrays. Nested values are prepared using matching anyOf or oneOf branches
101+ * and their property or item schemas.
102+ *
103+ * @since 7.2.0
104+ *
105+ * @param mixed $value The value to prepare.
106+ * @param array<string, mixed> $schema The schema describing the value.
107+ * @return mixed The prepared value.
108+ */
109+ function wp_prepare_json_value_for_client ( $ value , array $ schema ) {
110+ if ( $ value instanceof JsonSerializable ) {
111+ $ value = $ value ->jsonSerialize ();
112+ }
113+
114+ if ( ! is_array ( $ value ) && ! $ value instanceof stdClass ) {
115+ return $ value ;
116+ }
117+
118+ foreach ( array ( 'anyOf ' , 'oneOf ' ) as $ keyword ) {
119+ if ( ! isset ( $ schema [ $ keyword ] ) ) {
120+ continue ;
121+ }
122+
123+ $ matching_schema = null ;
124+
125+ /*
126+ * Prefer an array schema for PHP lists. An empty PHP array also matches an
127+ * object schema, but should remain [] when an array schema accepts it.
128+ */
129+ if ( is_array ( $ value ) && array_is_list ( $ value ) ) {
130+ foreach ( $ schema [ $ keyword ] as $ candidate_schema ) {
131+ if ( ! isset ( $ candidate_schema ['type ' ] ) && isset ( $ schema ['type ' ] ) ) {
132+ $ candidate_schema ['type ' ] = $ schema ['type ' ];
133+ }
134+
135+ if ( _wp_json_schema_matches_type ( $ value , $ candidate_schema , 'array ' ) ) {
136+ $ matching_schema = $ candidate_schema ;
137+ break ;
138+ }
139+ }
140+ }
141+
142+ // Like rest_filter_response_by_context(), use the first matching oneOf branch.
143+ if ( null === $ matching_schema ) {
144+ $ matching_schema = 'anyOf ' === $ keyword
145+ ? rest_find_any_matching_schema ( $ value , $ schema , '' )
146+ : rest_find_one_matching_schema ( $ value , $ schema , '' , true );
147+ }
148+
149+ if ( is_wp_error ( $ matching_schema ) ) {
150+ continue ;
151+ }
152+
153+ if ( ! isset ( $ schema ['type ' ] ) && isset ( $ matching_schema ['type ' ] ) ) {
154+ $ schema ['type ' ] = $ matching_schema ['type ' ];
155+ }
156+
157+ $ value = wp_prepare_json_value_for_client ( $ value , $ matching_schema );
158+ }
159+
160+ $ is_array_value = is_array ( $ value );
161+ $ types = isset ( $ schema ['type ' ] ) ? (array ) $ schema ['type ' ] : array ();
162+ $ allows_object = in_array ( 'object ' , $ types , true );
163+ $ allows_array = in_array ( 'array ' , $ types , true );
164+
165+ if ( $ is_array_value && empty ( $ value ) && $ allows_object && ! $ allows_array ) {
166+ return (object ) $ value ;
167+ }
168+
169+ $ is_object_value = $ value instanceof stdClass || ( $ is_array_value && $ allows_object && ( ! $ allows_array || ! array_is_list ( $ value ) ) );
170+ if ( $ allows_object && $ is_object_value ) {
171+ if ( $ value instanceof stdClass ) {
172+ $ value = clone $ value ;
173+ }
174+
175+ foreach ( $ value as $ property => $ property_value ) {
176+ $ property_schema = null ;
177+ if ( isset ( $ schema ['properties ' ][ $ property ] ) && is_array ( $ schema ['properties ' ][ $ property ] ) ) {
178+ $ property_schema = $ schema ['properties ' ][ $ property ];
179+ } else {
180+ $ property_schema = rest_find_matching_pattern_property_schema ( $ property , $ schema );
181+ if ( null === $ property_schema && isset ( $ schema ['additionalProperties ' ] ) && is_array ( $ schema ['additionalProperties ' ] ) ) {
182+ $ property_schema = $ schema ['additionalProperties ' ];
183+ }
184+ }
185+
186+ if ( null === $ property_schema ) {
187+ continue ;
188+ }
189+
190+ $ property_value = wp_prepare_json_value_for_client ( $ property_value , $ property_schema );
191+ if ( is_array ( $ value ) ) {
192+ $ value [ $ property ] = $ property_value ;
193+ } else {
194+ $ value ->$ property = $ property_value ;
195+ }
196+ }
197+ } elseif ( $ allows_array && $ is_array_value && isset ( $ schema ['items ' ] ) && is_array ( $ schema ['items ' ] ) ) {
198+ foreach ( $ value as $ index => $ item ) {
199+ $ value [ $ index ] = wp_prepare_json_value_for_client ( $ item , $ schema ['items ' ] );
200+ }
201+ }
202+
203+ return $ value ;
204+ }
205+
96206/**
97207 * Prepares a JSON Schema for clients using a given keyword lookup.
98208 *
@@ -199,3 +309,56 @@ function _wp_prepare_json_schema_for_client_with_allowed_keywords( array $schema
199309
200310 return $ schema ;
201311}
312+
313+ /**
314+ * Determines whether a value matches a JSON Schema through a given type.
315+ *
316+ * Combining schemas must have a matching branch that allows the requested type.
317+ * Finding the type in one branch and validating the value against another branch
318+ * is not sufficient.
319+ *
320+ * @since 7.2.0
321+ * @access private
322+ *
323+ * @param mixed $value The value to validate.
324+ * @param array<string, mixed> $schema The schema to inspect.
325+ * @param string $type The type to match.
326+ * @return bool Whether the value matches the schema through the requested type.
327+ */
328+ function _wp_json_schema_matches_type ( $ value , array $ schema , string $ type ): bool {
329+ if ( isset ( $ schema ['type ' ] ) && ! in_array ( $ type , (array ) $ schema ['type ' ], true ) ) {
330+ return false ;
331+ }
332+
333+ foreach ( array ( 'anyOf ' , 'oneOf ' ) as $ keyword ) {
334+ if ( ! isset ( $ schema [ $ keyword ] ) ) {
335+ continue ;
336+ }
337+
338+ foreach ( $ schema [ $ keyword ] as $ sub_schema ) {
339+ if ( ! is_array ( $ sub_schema ) ) {
340+ continue ;
341+ }
342+
343+ // Match rest_find_any_matching_schema(), which applies the parent type to untyped branches.
344+ if ( ! isset ( $ sub_schema ['type ' ] ) && isset ( $ schema ['type ' ] ) ) {
345+ $ sub_schema ['type ' ] = $ schema ['type ' ];
346+ }
347+
348+ if ( _wp_json_schema_matches_type ( $ value , $ sub_schema , $ type ) ) {
349+ continue 2 ;
350+ }
351+ }
352+
353+ return false ;
354+ }
355+
356+ if ( ! isset ( $ schema ['type ' ] ) && ! isset ( $ schema ['anyOf ' ] ) && ! isset ( $ schema ['oneOf ' ] ) ) {
357+ return false ;
358+ }
359+
360+ // Explicit and inferred types must both validate through the requested type only.
361+ $ schema ['type ' ] = $ type ;
362+
363+ return ! is_wp_error ( rest_validate_value_from_schema ( $ value , $ schema ) );
364+ }
0 commit comments