read reads a record (line by default, but ksh93/bash/zsh allow other delimiters with -d, even NUL with zsh/bash) and returns success as long as a full record has been read.
read returns non-zero when it finds EOF while the record delimiter has still not been encountered.
That allows you do do things like
while IFS= read -r line; do
...
done < text-file
Or with zsh/bash
while IFS= read -rd '' nul_delimited_record; do
...
done < null-delimited-list
And that loop to exit after the last record has been read.
You can still check if there was more data after the last full record with [ -n "$nul_delimited_record" ].
In your case, read's input doesn't contain any record as it doesn't contain any NUL character. In bash, it's not possible to embed a NUL inside a here document. So read fails because it hasn't managed to read a full record. It stills stores what it has read until EOF (after IFS processing) in the json variable.
In any case, using read without setting $IFS rarely makes sense.
For more details, see Understanding "IFS= read -r line".
while read -r line || [[ -n $line ]]mean?grepwhen it doesn't find a match,cmpanddiffwhen the files don't match, even(( someexpression ))when the expression evaluates to 0 ("false" in arithmetic context).readbehavior does make sense, because it failed to read a complete record (including terminator) from its input.set -eroutinely.