Index: src/wp-includes/rest-api/class-wp-rest-request.php
===================================================================
--- src/wp-includes/rest-api/class-wp-rest-request.php	(revision 39408)
+++ src/wp-includes/rest-api/class-wp-rest-request.php	(working copy)
@@ -905,6 +905,11 @@
 			}
 		}
 
+		$meta_param = $this->get_param( 'meta' );
+		if ( isset( $meta_param ) && ! is_array( $meta_param ) ) {
+			$invalid_params['meta'] = __( 'Invalid parameter.' );
+		}
+
 		if ( $invalid_params ) {
 			return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), array( 'status' => 400, 'params' => $invalid_params ) );
 		}
Index: src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php
===================================================================
--- src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php	(revision 39408)
+++ src/wp-includes/rest-api/fields/class-wp-rest-meta-fields.php	(working copy)
@@ -117,15 +117,19 @@
 	 * @since 4.7.0
 	 * @access public
 	 *
-	 * @param WP_REST_Request $request   Full details about the request.
+	 * @param array           $meta      Array of meta parsed from the request.
 	 * @param int             $object_id Object ID to fetch meta for.
 	 * @return WP_Error|null WP_Error if one occurs, null on success.
 	 */
-	public function update_value( $request, $object_id ) {
+	public function update_value( $meta, $object_id ) {
+		if ( ! is_array( $meta ) ) {
+			return new WP_Error( 'rest_invalid_param', sprintf( __( '%1$s is not of type %2$s.' ), 'meta', 'array' ), array( 'status' => 400 ) );
+		}
+
 		$fields = $this->get_registered_fields();
 		foreach ( $fields as $meta_key => $args ) {
 			$name = $args['name'];
-			if ( ! array_key_exists( $name, $request ) ) {
+			if ( ! array_key_exists( $name, $meta ) ) {
 				continue;
 			}
 
@@ -133,7 +137,7 @@
 			 * A null value means reset the field, which is essentially deleting it
 			 * from the database and then relying on the default value.
 			 */
-			if ( is_null( $request[ $name ] ) ) {
+			if ( is_null( $meta[ $name ] ) ) {
 				$result = $this->delete_meta_value( $object_id, $meta_key, $name );
 				if ( is_wp_error( $result ) ) {
 					return $result;
@@ -141,13 +145,13 @@
 				continue;
 			}
 
-			$is_valid = rest_validate_value_from_schema( $request[ $name ], $args['schema'], 'meta.' . $name );
+			$is_valid = rest_validate_value_from_schema( $meta[ $name ], $args['schema'], 'meta.' . $name );
 			if ( is_wp_error( $is_valid ) ) {
 				$is_valid->add_data( array( 'status' => 400 ) );
 				return $is_valid;
 			}
 
-			$value = rest_sanitize_value_from_schema( $request[ $name ], $args['schema'] );
+			$value = rest_sanitize_value_from_schema( $meta[ $name ], $args['schema'] );
 
 			if ( $args['single'] ) {
 				$result = $this->update_meta_value( $object_id, $meta_key, $name, $value );
Index: tests/phpunit/tests/rest-api/rest-post-meta-fields.php
===================================================================
--- tests/phpunit/tests/rest-api/rest-post-meta-fields.php	(revision 39408)
+++ tests/phpunit/tests/rest-api/rest-post-meta-fields.php	(working copy)
@@ -786,6 +786,65 @@
 		$this->assertContains( 'graeme', $meta );
 	}
 
+	/**
+	 * @ticket 38989
+	 */
+	public function test_set_value_invalid_meta_string_request_type() {
+		update_post_meta( self::$post_id, 'test_single', 'So I tied an onion to my belt, which was the style at the time.' );
+		$post_original = get_post( self::$post_id );
+
+		$this->grant_write_permission();
+
+		$data = array(
+			'title' => 'Ignore this title',
+			'meta'  => 'Not an array.',
+		);
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+		// The meta value should not have changed.
+		$current_value = get_post_meta( self::$post_id, 'test_single', true );
+		$this->assertEquals( 'So I tied an onion to my belt, which was the style at the time.', $current_value );
+
+		// Ensure the post title update was not processed.
+		$post_updated = get_post( self::$post_id );
+		$this->assertEquals( $post_original->post_title, $post_updated->post_title );
+	}
+
+	/**
+	 * @ticket 38989
+	 */
+	public function test_set_value_invalid_meta_float_request_type() {
+		update_post_meta( self::$post_id, 'test_single', 'Now, to take the ferry cost a nickel, and in those days, nickels had pictures of bumblebees on them.' );
+		$post_original = get_post( self::$post_id );
+
+		$this->grant_write_permission();
+
+		$data = array(
+			'content' => 'Ignore this content.',
+			'meta'    => 1.234,
+		);
+
+		$request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) );
+		$request->set_body_params( $data );
+
+		$response = $this->server->dispatch( $request );
+		$this->assertErrorResponse( 'rest_invalid_param', $response, 400 );
+
+		// The meta value should not have changed.
+		$current_value = get_post_meta( self::$post_id, 'test_single', true );
+		$this->assertEquals( 'Now, to take the ferry cost a nickel, and in those days, nickels had pictures of bumblebees on them.', $current_value );
+
+		// Ensure the post content update was not processed.
+		$post_updated = get_post( self::$post_id );
+		$this->assertEquals( $post_original->post_content, $post_updated->post_content );
+	}
+
 	public function test_remove_multi_value_db_error() {
 		add_post_meta( self::$post_id, 'test_multi', 'val1' );
 		$values = get_post_meta( self::$post_id, 'test_multi', false );
