Index: src/wp-includes/rest-api/class-wp-rest-request.php
===================================================================
--- src/wp-includes/rest-api/class-wp-rest-request.php	(revision 39107)
+++ src/wp-includes/rest-api/class-wp-rest-request.php	(working copy)
@@ -651,11 +651,13 @@
 	 * Avoids parsing the JSON data until we need to access it.
 	 *
 	 * @since 4.4.0
+	 * @since 4.7.0 Returns error instance if value cannot be decoded.
 	 * @access protected
+	 * @return true|WP_Error True if the JSON data was passed or no JSON data was provided, WP_Error if invalid JSON was passed.
 	 */
 	protected function parse_json_params() {
 		if ( $this->parsed_json ) {
-			return;
+			return true;
 		}
 
 		$this->parsed_json = true;
@@ -664,7 +666,7 @@
 		$content_type = $this->get_content_type();
 
 		if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
-			return;
+			return true;
 		}
 
 		$params = json_decode( $this->get_body(), true );
@@ -676,10 +678,19 @@
 		 * might not be defined: https://core.trac.wordpress.org/ticket/27799
 		 */
 		if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
-			return;
+			// Ensure subsequent calls receive error instance.
+			$this->parsed_json = false;
+
+			$error_data = array(
+				'status' => WP_Http::BAD_REQUEST,
+				'json_error_code' => json_last_error(),
+				'json_error_message' => json_last_error_msg(),
+			);
+			return new WP_Error( 'rest_invalid_json', __( 'Invalid JSON body passed.' ), $error_data );
 		}
 
 		$this->params['JSON'] = $params;
+		return true;
 	}
 
 	/**
@@ -841,6 +852,12 @@
 	 *                       WP_Error if required parameters are missing.
 	 */
 	public function has_valid_params() {
+		// If JSON data was passed, check for errors.
+		$json_error = $this->parse_json_params();
+		if ( is_wp_error( $json_error ) ) {
+			return $json_error;
+		}
+
 		$attributes = $this->get_attributes();
 		$required = array();
 
Index: tests/phpunit/tests/rest-api/rest-request.php
===================================================================
--- tests/phpunit/tests/rest-api/rest-request.php	(revision 39107)
+++ tests/phpunit/tests/rest-api/rest-request.php	(working copy)
@@ -399,6 +399,21 @@
 		$this->assertEquals( 'rest_invalid_param', $valid->get_error_code() );
 	}
 
+	public function test_has_valid_params_json_error() {
+		if ( version_compare( PHP_VERSION, '5.3', '<' ) ) {
+			return $this->markTestSkipped( 'JSON validation is only available for PHP 5.3+' );
+		}
+
+		$this->request->set_header( 'Content-Type', 'application/json' );
+		$this->request->set_body( '{"invalid": JSON}' );
+
+		$valid = $this->request->has_valid_params();
+		$this->assertWPError( $valid );
+		$this->assertEquals( 'rest_invalid_json', $valid->get_error_code() );
+		$data = $valid->get_error_data();
+		$this->assertEquals( JSON_ERROR_SYNTAX, $data['json_error_code'] );
+	}
+
 	public function test_has_multiple_invalid_params_validate_callback() {
 		$this->request->set_url_params( array(
 			'someinteger' => '123',
