grafillo, уясните простое - РНР принимает POST данные как пары ключ=>значение и если таковых не обнаружит, то вернет пустой массив $_POST. Это и происходит когда вы отдаете на сервер просто строку - json. В этом случае нужно обрабатывать сырые данные, то что пришло, самостоятельно, как по ссылке.
Но если json строку передавать как значение ключа, то ее можно получить из $_POST.
Если же нужно отправить объект, то его нужно серелизовать, можно так:
var data = {
name: "Вася",
surname: "Петров"
};
//а это для отправки
var send_data = Object.entries(data).map( m => m.join('=')).join('&'); //name=Вася&surname=Петров
xhr.send(send_data);
Вот теперь массив $_POST будет заполнен.