Index: src/wp-includes/functions.php
===================================================================
--- src/wp-includes/functions.php	(revision 29021)
+++ src/wp-includes/functions.php	(working copy)
@@ -2612,6 +2612,48 @@
 }
 
 /**
+ * Encode a variable into JSON, with some sanity checks
+ *
+ * @since 4.0
+ * 
+ * @param mixed $data Variable (usually an array or object) to encode as JSON
+ *
+ * @return string The JSON encoded string
+ */
+function wp_json_encode( $data ) {
+	$try = json_encode( $data );
+
+	if ( ! empty( $try ) ) {
+		// If json_encode was successful, no need to do more sanity checking
+		return $try;
+	}
+
+	return json_encode( _wp_json_sanity_check( $data ) );
+}
+
+function _wp_json_sanity_check( $data ) {
+	if ( is_array( $data ) ) {
+		foreach ( $data as $id => $el ) {
+			$data[ $id ] = _wp_json_sanity_check( $el );
+		}
+	} else if ( is_object( $data ) ) {
+		foreach ( $data as $id => $el ) {
+			$data->$id = _wp_json_sanity_check( $el );
+		}
+	} else if ( function_exists( 'mb_convert_encoding' ) ) {
+		if ( mb_detect_encoding( $data ) ) {
+			$data = mb_convert_encoding( $data, 'UTF-8', mb_detect_encoding( $data ) );
+		} else {
+			$data = mb_convert_encoding( $data, 'UTF-8', 'auto' );
+		}
+	} else {
+		$data = wp_check_invalid_utf8( $data, true );
+	}
+
+	return $data;
+}
+
+/**
  * Send a JSON response back to an Ajax request.
  *
  * @since 3.5.0
@@ -2621,7 +2663,7 @@
  */
 function wp_send_json( $response ) {
 	@header( 'Content-Type: application/json; charset=' . get_option( 'blog_charset' ) );
-	echo json_encode( $response );
+	echo wp_json_encode( $response );
 	if ( defined( 'DOING_AJAX' ) && DOING_AJAX )
 		wp_die();
 	else
Index: tests/phpunit/tests/functions.php
===================================================================
--- tests/phpunit/tests/functions.php	(revision 29021)
+++ tests/phpunit/tests/functions.php	(working copy)
@@ -524,4 +524,37 @@
 		$this->assertCount( 8, $urls );
 		$this->assertEquals( array_slice( $original_urls, 0, 8 ), $urls );
 	}
+
+	/**
+	 * @ticket 28786
+	 */
+	function test_wp_json_encode() {
+		$tests = array(
+			'a' => '"a"',
+			'这' => '"\u8fd9"',
+		);
+		
+		foreach ( $tests as $string => $json ) {
+			$this->assertEquals( wp_json_encode( $string ), $json );
+		}
+
+		$charsets = mb_detect_order();
+		if ( ! in_array( 'EUC-JP', $charsets ) ) {
+			$charsets[] = 'EUC-JP';
+			mb_detect_order( $charsets );
+		}
+
+		$eucjp = mb_convert_encoding( 'aあb', 'EUC-JP', 'UTF-8' );
+		$utf8 = mb_convert_encoding( $eucjp, 'UTF-8', 'EUC-JP' );
+
+		$this->assertEquals( 'aあb', $utf8 );
+
+		$this->assertEquals( wp_json_encode( $eucjp ), '"a\u3042b"' );
+
+		$this->assertEquals( wp_json_encode( array( 'a' ) ), '["a"]' );
+
+		$object = new stdClass;
+		$object->a = 'b';
+		$this->assertEquals( wp_json_encode( $object ), '{"a":"b"}' );
+	}
 }
