ExportString JSON produces UTF8 encoded json string, if put as HTTPRequest body it will undergo another encoding unless you prevent it with an option:
HTTPRequest[..., CharacterEncoding -> None]
It is analogous to the double decoding issue which you address with bodybytes//FromCharacterCode//ImportString.
Exchanging JSON via http requests
This is very common these days so for the record:
Building requests (for URLRead input):
HTTPRequest[ url_ , <| "ContentType" -> "application/json" , "Body" -> ExportString[ jsonCompliantExpr, "RawJSON"] , "Method" -> "POST" , "Headers" -> {"Accept"->"application/json"} (* less important *) |> , CharacterEncoding -> None ]
Receiving requests ( for APIFunction )
I will skip the part of handling different payloads or error handling, just assuming the incomming request is on generated as above will need:
APIFunction[ {} , Function[whatever (*we don't care about query params*) , HTTPRequestData["BodyBytes"] // FromCharacterCode // ImportString[#, "RawJSON"]& // doSomethingWithAssoAndGenerateResponse ] ]
Building responses (* in API/FormFunctions *)
For APIFunction etc there is a nice wrapper ExportForm which works with GenerateHTTPResponse which is called on the result of APIFunction's function.
So a quick way:
APIFunction[{}, ExportForm[association, "RawJSON" ]& ]
And a longer way:
HTTPResponse[ ExportString[association_ , "RawJSON" ] , <| "ContentType"->"application/json" |> , CharacterEncoding -> None ]
I wish there was ExportForm support for building HTTPRequests, I even asked WRI, the status is 'maybe'.
Parsing responses (* what URLRead needs to do *)
Worth to mention that URLExecute does it well wrt encoding but I always need more controll on status codes etc so I need to use URLRead. So as you mentioned:
URLRead[...]["BodyBytes"]& // FromCharacterCode//ImportString[#, "RawJSON"]&
I would not try to decode "Body" because it is very unclear what the body is according to developers, the behavior of "Body" worsened in 12.2 it became even less predictable. So URLExecute or "BodyBytes" are the way to go.
More reading:
HTTPRequest syntax problem
Who is to blame: parsing UTF8 encoded JSON HTTPResponse fails