esc_attr() corrupts json values

Normally, when one want to put strings to be used in javascript, esc_js is the right function, not esc_attr.

The problem is that esc_js, according to docs:

Escapes text strings for echoing in JS

(bold mine).

So, using with esc_js you obtain a string that can be safely echoed in js, not parsed: it’s not a bug, it’s the intended behaviour.

The canonical way to pass data from PHP to js in WordPress is wp_localize_script so you should consider to use it instead. (Note that function internally do not use any esc_* functions, just echo the result of json_encode).

As alternative I can suggest you to use filter_var with sanitize filters: IMHO it is a far better way to do the task esc_* WP functions do.
Consider that esc_* function pass through filter hooks, so integrity of your data can be corrupted by external code.

Try:

<?php
$data_safe = filter_var( $json, FILTER_SANITIZE_SPECIAL_CHARS );
$out_safe = filter_var( $json, FILTER_SANITIZE_FULL_SPECIAL_CHARS );
?>
<textarea data-test-json="<?php echo $data_safe; ?>"><?php echo $out_safe; ?></textarea>