Index: src/wp-includes/functions.php
===================================================================
--- src/wp-includes/functions.php	(revision 29720)
+++ src/wp-includes/functions.php	(working copy)
@@ -2612,6 +2612,78 @@
 }
 
 /**
+ * Encode a variable into JSON, with some sanity checks
+ *
+ * @since 4.1
+ *
+ * @param mixed $data    Variable (usually an array or object) to encode as JSON
+ * @param int   $options Options to be passed to json_encode(). Default 0.
+ * @param int   $depth   Maximum depth to walk through $data. Must be greater than 0, default 512.t
+ *
+ * @return bool|string The JSON encoded string, or false if it cannot be encoded
+ */
+function wp_json_encode( $data, $options = 0, $depth = 512 ) {
+	// json_encode has had extra params added over the years.
+	// $options was added in 5.3, and $depth in 5.5.
+	// We need to make sure we call it with the correct arguments.
+	if ( version_compare( PHP_VERSION, '5.5', '>=' ) ) {
+		$args = array( $data, $options, $depth );
+	} else if ( version_compare( PHP_VERSION, '5.3', '>=' ) ) {
+		$args = array( $data, $options );
+	} else {
+		$args = array( $data );
+	}
+
+	$json = call_user_func_array( 'json_encode', $args );
+
+	if ( false !== $json ) {
+		// If json_encode was successful, no need to do more sanity checking
+		return $json;
+	}
+
+	try {
+		$args[0] = _wp_json_sanity_check( $data, $depth );
+	} catch ( Exception $e ) {
+		return false;
+	}
+
+	return call_user_func_array( 'json_encode', $args );
+}
+
+function _wp_json_sanity_check( $data, $depth ) {
+	if ( $depth < 0 ) {
+		throw new Exception( 'Reached depth limit' );
+	}
+
+	if ( is_array( $data ) ) {
+		foreach ( $data as $id => $el ) {
+			if ( is_array( $el ) || is_object( $el ) || is_string( $el ) ) {
+				$data[ $id ] = _wp_json_sanity_check( $el, $depth - 1 );
+			}
+		}
+	} else if ( is_object( $data ) ) {
+		foreach ( $data as $id => $el ) {
+			if ( is_array( $el ) || is_object( $el ) || is_string( $el ) ) {
+				$data->$id = _wp_json_sanity_check( $el, $depth - 1 );
+			}
+		}
+	} else if ( is_string( $data ) ) {
+		if ( function_exists( 'mb_convert_encoding' ) ) {
+			$encoding = mb_detect_encoding( $data );
+			if ( $encoding ) {
+				$data = mb_convert_encoding( $data, 'UTF-8', $encoding );
+			} else {
+				$data = mb_convert_encoding( $data, 'UTF-8', 'UTF-8' );
+			}
+		} 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 +2693,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: src/wp-includes/media.php
===================================================================
--- src/wp-includes/media.php	(revision 29720)
+++ src/wp-includes/media.php	(working copy)
@@ -1399,7 +1399,7 @@
 	}
 	?></ol>
 	</noscript>
-	<script type="application/json" class="wp-playlist-script"><?php echo json_encode( $data ) ?></script>
+	<script type="application/json" class="wp-playlist-script"><?php echo wp_json_encode( $data ) ?></script>
 </div>
 	<?php
 	return ob_get_clean();
@@ -2575,7 +2575,7 @@
 		'limitExceeded' => is_multisite() && ! is_upload_space_available()
 	);
 
-	$script = 'var _wpPluploadSettings = ' . json_encode( $settings ) . ';';
+	$script = 'var _wpPluploadSettings = ' . wp_json_encode( $settings ) . ';';
 
 	if ( $data )
 		$script = "$data\n$script";
Index: src/wp-includes/update.php
===================================================================
--- src/wp-includes/update.php	(revision 29720)
+++ src/wp-includes/update.php	(working copy)
@@ -94,7 +94,7 @@
 	);
 
 	$post_body = array(
-		'translations' => json_encode( $translations ),
+		'translations' => wp_json_encode( $translations ),
 	);
 
 	if ( is_array( $extra_stats ) )
@@ -274,16 +274,16 @@
 	$options = array(
 		'timeout' => $timeout,
 		'body' => array(
-			'plugins'      => json_encode( $to_send ),
-			'translations' => json_encode( $translations ),
-			'locale'       => json_encode( $locales ),
-			'all'          => json_encode( true ),
+			'plugins'      => wp_json_encode( $to_send ),
+			'translations' => wp_json_encode( $translations ),
+			'locale'       => wp_json_encode( $locales ),
+			'all'          => wp_json_encode( true ),
 		),
 		'user-agent' => 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' )
 	);
 
 	if ( $extra_stats ) {
-		$options['body']['update_stats'] = json_encode( $extra_stats );
+		$options['body']['update_stats'] = wp_json_encode( $extra_stats );
 	}
 
 	$url = $http_url = 'http://api.wordpress.org/plugins/update-check/1.1/';
@@ -437,15 +437,15 @@
 	$options = array(
 		'timeout' => $timeout,
 		'body' => array(
-			'themes'       => json_encode( $request ),
-			'translations' => json_encode( $translations ),
-			'locale'       => json_encode( $locales ),
+			'themes'       => wp_json_encode( $request ),
+			'translations' => wp_json_encode( $translations ),
+			'locale'       => wp_json_encode( $locales ),
 		),
 		'user-agent'	=> 'WordPress/' . $wp_version . '; ' . get_bloginfo( 'url' )
 	);
 
 	if ( $extra_stats ) {
-		$options['body']['update_stats'] = json_encode( $extra_stats );
+		$options['body']['update_stats'] = wp_json_encode( $extra_stats );
 	}
 
 	$url = $http_url = 'http://api.wordpress.org/themes/update-check/1.1/';
Index: tests/phpunit/tests/functions.php
===================================================================
--- tests/phpunit/tests/functions.php	(revision 29720)
+++ tests/phpunit/tests/functions.php	(working copy)
@@ -524,4 +524,46 @@
 		$this->assertCount( 8, $urls );
 		$this->assertEquals( array_slice( $original_urls, 0, 8 ), $urls );
 	}
+
+	/**
+	 * @ticket 28786
+	 */
+	function test_wp_json_encode() {
+		$this->assertEquals( wp_json_encode( 'a' ), '"a"' );
+		$this->assertEquals( wp_json_encode( '这' ), '"\u8fd9"' );
+
+		$old_charsets = $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"}' );
+
+		mb_detect_order( $old_charsets );
+	}
+
+	/**
+	 * @ticket 28786
+	 */
+	function test_wp_json_encode_depth() {
+		$data = array( array( array( 1, 2, 3 ) ) );
+		$json = wp_json_encode( $data, 0, 1 );
+		$this->assertFalse( $json );
+
+		$data = array( 'あ', array( array( 1, 2, 3 ) ) );
+		$json = wp_json_encode( $data, 0, 1 );
+		$this->assertFalse( $json );
+	}
 }
