Cross-posting visibility, and because I think it’s such an elegant solution:
import serdeJson "mo:serde/JSON";
type User = {
name: Text;
id: Nat;
};
let blob = serdeJson.fromText("{\"name\": \"bar\", \"id\": 112}");
let user : ?User = from_candid(blob);
assert user == ?{ name = "bar"; id = 112 };