diff --git src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
index a0752c2534..8fdc9b766a 100644
--- src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
+++ src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php
@@ -269,26 +269,52 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
 		}
 
 		foreach ( $taxonomies as $taxonomy ) {
-			$base        = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
-			$tax_exclude = $base . '_exclude';
-
-			if ( ! empty( $request[ $base ] ) ) {
-				$args['tax_query'][] = array(
-					'taxonomy'         => $taxonomy->name,
-					'field'            => 'term_id',
-					'terms'            => $request[ $base ],
-					'include_children' => false,
-				);
+			$base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
+
+			$tax_include = $request[ $base ];
+			$tax_exclude = $request[ $base . '_exclude' ];
+
+			if ( $tax_include ) {
+				$terms            = array();
+				$include_children = false;
+
+				if ( rest_is_array( $tax_include ) ) {
+					$terms = $tax_include;
+				} elseif ( rest_is_object( $tax_include ) ) {
+					$terms            = empty( $tax_include['terms'] ) ? array() : $tax_include['terms'];
+					$include_children = ! empty( $tax_include['include_children'] );
+				}
+
+				if ( $terms ) {
+					$args['tax_query'][] = array(
+						'taxonomy'         => $taxonomy->name,
+						'field'            => 'term_id',
+						'terms'            => $terms,
+						'include_children' => $include_children,
+					);
+				}
 			}
 
-			if ( ! empty( $request[ $tax_exclude ] ) ) {
-				$args['tax_query'][] = array(
-					'taxonomy'         => $taxonomy->name,
-					'field'            => 'term_id',
-					'terms'            => $request[ $tax_exclude ],
-					'include_children' => false,
-					'operator'         => 'NOT IN',
-				);
+			if ( $tax_exclude ) {
+				$terms            = array();
+				$include_children = false;
+
+				if ( rest_is_array( $tax_exclude ) ) {
+					$terms = $tax_exclude;
+				} elseif ( rest_is_object( $tax_exclude ) ) {
+					$terms            = empty( $tax_exclude['terms'] ) ? array() : $tax_exclude['terms'];
+					$include_children = ! empty( $tax_exclude['include_children'] );
+				}
+
+				if ( $terms ) {
+					$args['tax_query'][] = array(
+						'taxonomy'         => $taxonomy->name,
+						'field'            => 'term_id',
+						'terms'            => $terms,
+						'include_children' => $include_children,
+						'operator'         => 'NOT IN',
+					);
+				}
 			}
 		}
 
@@ -2783,21 +2809,65 @@ class WP_REST_Posts_Controller extends WP_REST_Controller {
 			$query_params[ $base ] = array(
 				/* translators: %s: Taxonomy name. */
 				'description' => sprintf( __( 'Limit result set to all items that have the specified term assigned in the %s taxonomy.' ), $base ),
-				'type'        => 'array',
-				'items'       => array(
-					'type' => 'integer',
+				'type'        => array( 'object', 'array' ),
+				'oneOf'       => array(
+					array(
+						'type'    => 'array',
+						'items'   => array(
+							'type' => 'integer',
+						),
+						'default' => array(),
+					),
+					array(
+						'type'                 => 'object',
+						'properties'           => array(
+							'terms'            => array(
+								'type'    => 'array',
+								'items'   => array(
+									'type' => 'integer',
+								),
+								'default' => array(),
+							),
+							'include_children' => array(
+								'type'    => 'boolean',
+								'default' => false,
+							),
+						),
+						'additionalProperties' => false,
+					),
 				),
-				'default'     => array(),
 			);
 
 			$query_params[ $base . '_exclude' ] = array(
 				/* translators: %s: Taxonomy name. */
 				'description' => sprintf( __( 'Limit result set to all items except those that have the specified term assigned in the %s taxonomy.' ), $base ),
-				'type'        => 'array',
-				'items'       => array(
-					'type' => 'integer',
+				'type'        => array( 'object', 'array' ),
+				'oneOf'       => array(
+					array(
+						'type'    => 'array',
+						'items'   => array(
+							'type' => 'integer',
+						),
+						'default' => array(),
+					),
+					array(
+						'type'                 => 'object',
+						'properties'           => array(
+							'terms'            => array(
+								'type'    => 'array',
+								'items'   => array(
+									'type' => 'integer',
+								),
+								'default' => array(),
+							),
+							'include_children' => array(
+								'type'    => 'boolean',
+								'default' => false,
+							),
+						),
+						'additionalProperties' => false,
+					),
 				),
-				'default'     => array(),
 			);
 		}
 
diff --git tests/phpunit/tests/rest-api/rest-posts-controller.php tests/phpunit/tests/rest-api/rest-posts-controller.php
index d07347f973..cea5ef7961 100644
--- tests/phpunit/tests/rest-api/rest-posts-controller.php
+++ tests/phpunit/tests/rest-api/rest-posts-controller.php
@@ -1113,6 +1113,185 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te
 		$this->assertSame( $id1, $data[2]['id'] );
 	}
 
+	/**
+	 * @ticket 39494
+	 */
+	public function test_get_items_with_category_including_children() {
+		$taxonomy = get_taxonomy( 'category' );
+
+		$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
+		$cat2 = static::factory()->term->create(
+			array(
+				'taxonomy' => $taxonomy->name,
+				'parent'   => $cat1,
+			)
+		);
+
+		$post_ids = array(
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat1 ),
+				)
+			),
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat2 ),
+				)
+			),
+		);
+
+		$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+		$request->set_param(
+			$taxonomy->rest_base,
+			array(
+				'terms'            => array( $cat1 ),
+				'include_children' => true,
+			)
+		);
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertSame( $post_ids, array_column( $data, 'id' ) );
+	}
+
+	/**
+	 * @ticket 39494
+	 */
+	public function test_get_items_with_category_excluding_children() {
+		$taxonomy = get_taxonomy( 'category' );
+
+		$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
+		$cat2 = static::factory()->term->create(
+			array(
+				'taxonomy' => $taxonomy->name,
+				'parent'   => $cat1,
+			)
+		);
+
+		$post_ids = array(
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat1 ),
+				)
+			),
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat2 ),
+				)
+			),
+		);
+
+		$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+		$request->set_param(
+			$taxonomy->rest_base,
+			array(
+				'terms'            => array( $cat1 ),
+				'include_children' => false,
+			)
+		);
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertCount( 1, $data );
+		$this->assertEquals( $post_ids[0], $data[0]['id'] );
+	}
+
+	/**
+	 * @ticket 39494
+	 */
+	public function test_get_items_without_category_or_its_children() {
+		$taxonomy = get_taxonomy( 'category' );
+
+		$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
+		$cat2 = static::factory()->term->create(
+			array(
+				'taxonomy' => $taxonomy->name,
+				'parent'   => $cat1,
+			)
+		);
+
+		$post_ids = array(
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat1 ),
+				)
+			),
+			static::factory()->post->create(
+				array(
+					'post_status'   => 'publish',
+					'post_category' => array( $cat2 ),
+				)
+			),
+		);
+
+		$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+		$request->set_param(
+			$taxonomy->rest_base . '_exclude',
+			array(
+				'terms'            => array( $cat1 ),
+				'include_children' => true,
+			)
+		);
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$this->assertEmpty(
+			array_intersect(
+				$post_ids,
+				array_column( $data, 'id' )
+			)
+		);
+	}
+
+	/**
+	 * @ticket 39494
+	 */
+	public function test_get_items_without_category_but_allowing_its_children() {
+		$taxonomy = get_taxonomy( 'category' );
+
+		$cat1 = static::factory()->term->create( array( 'taxonomy' => $taxonomy->name ) );
+		$cat2 = static::factory()->term->create(
+			array(
+				'taxonomy' => $taxonomy->name,
+				'parent'   => $cat1,
+			)
+		);
+
+		$p1 = static::factory()->post->create(
+			array(
+				'post_status'   => 'publish',
+				'post_category' => array( $cat1 ),
+			)
+		);
+		$p2 = static::factory()->post->create(
+			array(
+				'post_status'   => 'publish',
+				'post_category' => array( $cat2 ),
+			)
+		);
+
+		$request = new WP_REST_Request( 'GET', '/wp/v2/posts' );
+		$request->set_param(
+			$taxonomy->rest_base . '_exclude',
+			array(
+				'terms'            => array( $cat1 ),
+				'include_children' => false,
+			)
+		);
+		$response = rest_get_server()->dispatch( $request );
+		$data     = $response->get_data();
+
+		$found_ids = array_column( $data, 'id' );
+
+		$this->assertNotContains( $p1, $found_ids );
+		$this->assertContains( $p2, $found_ids );
+	}
+
 	/**
 	 * @ticket 44326
 	 */
