diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php
index c5093cc33b..263a8eea61 100644
--- a/src/wp-includes/rest-api.php
+++ b/src/wp-includes/rest-api.php
@@ -1561,3 +1561,60 @@ function rest_preload_api_request( $memo, $path ) {
 
 	return $memo;
 }
+
+
+/**
+ * Filters the response to remove any fields not available in the given context.
+ *
+ * @since 5.4.0
+ *
+ * @param array|object $data    The response data to modify.
+ * @param array        $schema  The schema for the endpoint used to filter the response.
+ * @param string       $context The requested context.
+ * @return array|object The filtered response data.
+ */
+function rest_filter_response_by_context( $data, $schema, $context ) {
+	if ( ! is_array( $data ) && ! is_object( $data ) ) {
+		return $data;
+	}
+
+	if ( isset( $schema['type'] ) ) {
+		$type = $schema['type'];
+	} elseif ( isset( $schema['properties'] ) ) {
+		$type = 'object'; // Back compat if a developer accidentally omitted the type.
+	} else {
+		return $data;
+	}
+
+	foreach ( $data as $key => $value ) {
+		$check = array();
+
+		if ( 'array' === $type || ( is_array( $type ) && in_array( 'array', $type, true ) ) ) {
+			$check = isset( $schema['items'] ) ? $schema['items'] : array();
+		} elseif ( 'object' === $type || ( is_array( $type ) && in_array( 'object', $type, true ) ) ) {
+			if ( isset( $schema['properties'][ $key ] ) ) {
+				$check = $schema['properties'][ $key ];
+			} elseif ( isset( $schema['additionalProperties'] ) && is_array( $schema['additionalProperties'] ) ) {
+				$check = $schema['additionalProperties'];
+			}
+		}
+
+		if ( isset( $check['context'] ) && ! in_array( $context, $check['context'], true ) ) {
+			if ( is_object( $data ) ) {
+				unset( $data->$key );
+			} else {
+				unset( $data[ $key ] );
+			}
+		} else {
+			$new_value = rest_filter_response_by_context( $value, $check, $context );
+
+			if ( is_object( $data ) ) {
+				$data->$key = $new_value;
+			} else {
+				$data[ $key ] = $new_value;
+			}
+		}
+	}
+
+	return $data;
+}
diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
index 90b97f0f3b..f90ed7cdb3 100644
--- a/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
+++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-controller.php
@@ -234,7 +234,7 @@ abstract class WP_REST_Controller {
 	 *
 	 * @since 4.7.0
 	 *
-	 * @param array  $data    Response data to fiter.
+	 * @param array  $data    Response data to filter.
 	 * @param string $context Context defined in the schema.
 	 * @return array Filtered response.
 	 */
@@ -242,32 +242,7 @@ abstract class WP_REST_Controller {
 
 		$schema = $this->get_item_schema();
 
-		foreach ( $data as $key => $value ) {
-			if ( empty( $schema['properties'][ $key ] ) || empty( $schema['properties'][ $key ]['context'] ) ) {
-				continue;
-			}
-
-			if ( ! in_array( $context, $schema['properties'][ $key ]['context'], true ) ) {
-				unset( $data[ $key ] );
-				continue;
-			}
-
-			if ( 'object' === $schema['properties'][ $key ]['type'] && ! empty( $schema['properties'][ $key ]['properties'] ) ) {
-				foreach ( $schema['properties'][ $key ]['properties'] as $attribute => $details ) {
-					if ( empty( $details['context'] ) ) {
-						continue;
-					}
-
-					if ( ! in_array( $context, $details['context'], true ) ) {
-						if ( isset( $data[ $key ][ $attribute ] ) ) {
-							unset( $data[ $key ][ $attribute ] );
-						}
-					}
-				}
-			}
-		}
-
-		return $data;
+		return rest_filter_response_by_context( $data, $schema, $context );
 	}
 
 	/**
diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php
index 1efb81a348..3943f8711b 100644
--- a/tests/phpunit/tests/rest-api.php
+++ b/tests/phpunit/tests/rest-api.php
@@ -906,4 +906,314 @@ class Tests_REST_API extends WP_UnitTestCase {
 		$this->assertEquals( '/wp/v2/posts', $request->get_route() );
 		$this->assertEquals( 'GET', $request->get_method() );
 	}
+
+	/**
+	 * @dataProvider _dp_rest_filter_response_by_context
+	 */
+	public function test_rest_filter_response_by_context( $schema, $data, $expected ) {
+		$this->assertEquals( $expected, rest_filter_response_by_context( $data, $schema, 'view' ) );
+	}
+
+	public function _dp_rest_filter_response_by_context() {
+		return array(
+			'default'                => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'first'  => array(
+							'type'    => 'string',
+							'context' => array( 'view', 'edit' ),
+						),
+						'second' => array(
+							'type'    => 'string',
+							'context' => array( 'edit' ),
+						),
+					),
+				),
+				array(
+					'first'  => 'a',
+					'second' => 'b',
+				),
+				array( 'first' => 'a' ),
+			),
+			'keeps missing context'  => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'first'  => array(
+							'type'    => 'string',
+							'context' => array( 'view', 'edit' ),
+						),
+						'second' => array(
+							'type' => 'string',
+						),
+					),
+				),
+				array(
+					'first'  => 'a',
+					'second' => 'b',
+				),
+				array(
+					'first'  => 'a',
+					'second' => 'b',
+				),
+			),
+			'removes empty context'  => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'first'  => array(
+							'type'    => 'string',
+							'context' => array( 'view', 'edit' ),
+						),
+						'second' => array(
+							'type'    => 'string',
+							'context' => array(),
+						),
+					),
+				),
+				array(
+					'first'  => 'a',
+					'second' => 'b',
+				),
+				array( 'first' => 'a' ),
+			),
+			'nested properties'      => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'parent' => array(
+							'type'       => 'object',
+							'context'    => array( 'view', 'edit' ),
+							'properties' => array(
+								'child'  => array(
+									'type'    => 'string',
+									'context' => array( 'view', 'edit' ),
+								),
+								'hidden' => array(
+									'type'    => 'string',
+									'context' => array( 'edit' ),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'parent' => array(
+						'child'  => 'hi',
+						'hidden' => 'there',
+					),
+				),
+				array( 'parent' => array( 'child' => 'hi' ) ),
+			),
+			'grand child properties' => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'parent' => array(
+							'type'       => 'object',
+							'context'    => array( 'view', 'edit' ),
+							'properties' => array(
+								'child' => array(
+									'type'       => 'object',
+									'context'    => array( 'view', 'edit' ),
+									'properties' => array(
+										'grand'  => array(
+											'type'    => 'string',
+											'context' => array( 'view', 'edit' ),
+										),
+										'hidden' => array(
+											'type'    => 'string',
+											'context' => array( 'edit' ),
+										),
+									),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'parent' => array(
+						'child' => array(
+							'grand' => 'hi',
+						),
+					),
+				),
+				array( 'parent' => array( 'child' => array( 'grand' => 'hi' ) ) ),
+			),
+			'array'                  => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'arr' => array(
+							'type'    => 'array',
+							'context' => array( 'view', 'edit' ),
+							'items'   => array(
+								'type'       => 'object',
+								'context'    => array( 'view', 'edit' ),
+								'properties' => array(
+									'visible' => array(
+										'type'    => 'string',
+										'context' => array( 'view', 'edit' ),
+									),
+									'hidden'  => array(
+										'type'    => 'string',
+										'context' => array( 'edit' ),
+									),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'arr' => array(
+						array(
+							'visible' => 'hi',
+							'hidden'  => 'there',
+						),
+					),
+				),
+				array( 'arr' => array( array( 'visible' => 'hi' ) ) ),
+			),
+			'additional properties'  => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'additional' => array(
+							'type'                 => 'object',
+							'context'              => array( 'view', 'edit' ),
+							'properties'           => array(
+								'a' => array(
+									'type'    => 'string',
+									'context' => array( 'view', 'edit' ),
+								),
+								'b' => array(
+									'type'    => 'string',
+									'context' => array( 'edit' ),
+								),
+							),
+							'additionalProperties' => array(
+								'type'    => 'string',
+								'context' => array( 'edit' ),
+							),
+						),
+					),
+				),
+				array(
+					'additional' => array(
+						'a' => '1',
+						'b' => '2',
+						'c' => '3',
+					),
+				),
+				array( 'additional' => array( 'a' => '1' ) ),
+			),
+			'multiple types object'  => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'multi' => array(
+							'type'       => array( 'object', 'string' ),
+							'context'    => array( 'view', 'edit' ),
+							'properties' => array(
+								'a' => array(
+									'type'    => 'string',
+									'context' => array( 'view', 'edit' ),
+								),
+								'b' => array(
+									'type'    => 'string',
+									'context' => array( 'edit' ),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'multi' => array(
+						'a' => '1',
+						'b' => '2',
+					),
+				),
+				array( 'multi' => array( 'a' => '1' ) ),
+			),
+			'multiple types array'   => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'multi' => array(
+							'type'    => array( 'array', 'string' ),
+							'context' => array( 'view', 'edit' ),
+							'items'   => array(
+								'type'       => 'object',
+								'context'    => array( 'view', 'edit' ),
+								'properties' => array(
+									'visible' => array(
+										'type'    => 'string',
+										'context' => array( 'view', 'edit' ),
+									),
+									'hidden'  => array(
+										'type'    => 'string',
+										'context' => array( 'edit' ),
+									),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'multi' => array(
+						array(
+							'visible' => '1',
+							'hidden'  => '2',
+						),
+					),
+				),
+				array( 'multi' => array( array( 'visible' => '1' ) ) ),
+			),
+			'grand child properties traverses missing context' => array(
+				array(
+					'$schema'    => 'http://json-schema.org/draft-04/schema#',
+					'type'       => 'object',
+					'properties' => array(
+						'parent' => array(
+							'type'       => 'object',
+							'context'    => array( 'view', 'edit' ),
+							'properties' => array(
+								'child' => array(
+									'type'       => 'object',
+									'properties' => array(
+										'grand'  => array(
+											'type'    => 'string',
+											'context' => array( 'view', 'edit' ),
+										),
+										'hidden' => array(
+											'type'    => 'string',
+											'context' => array( 'edit' ),
+										),
+									),
+								),
+							),
+						),
+					),
+				),
+				array(
+					'parent' => array(
+						'child' => array(
+							'grand' => 'hi',
+						),
+					),
+				),
+				array( 'parent' => array( 'child' => array( 'grand' => 'hi' ) ) ),
+			),
+		);
+	}
 }
