Don't pretty print your API's JSON response body
This post's featured URL for sharing metadata is https://www.jvt.me/img/profile.jpg.
We generally build APIs for automated integrations, not for humans to read them. Although you as a human will look at the raw API request/response formats at some point, the ideal case is that you'll only be doing this as a much smaller percentage of the time that the API is called.
So something that slightly annoys me is seeing APIs which always return pretty-printed JSON objects, when that's something that I can do myself, as-and-when I need it.
In fact, I've ended up doing this across many different languages that I have 8(!) posts on how to pretty a JSON string because it's so common.
This is something that's been quite clear in my mind for a while, and as I've used a couple of APIs in the not so recent past, as well as a recent discussion on a feature request for oapi-codegen
, I thought it was worth writing up my thoughts.
My recommendation is that you do not send pretty-printed JSON at any point to your consumers.
Not only is it a larger message that requires a slightly increased amount of time to compress the data to be sent over the network (provided you are always compressing your responses, but it also takes more time to generate a pretty-printed JSON string than a non-pretty-printed one.
We can see some real numbers and impact by looking at the following Go benchmark:
package main
import (
"encoding/json"
"testing"
)
// via https://www.rfc-editor.org/rfc/rfc8259#section-13
type ComplexObject struct {
Image Image `json:"Image"`
}
type Image struct {
Width int `json:"Width"`
Height int `json:"Height"`
Title string `json:"Title"`
Thumbnail Thumbnail `json:"Thumbnail"`
Animated bool `json:"Animated"`
IDs []int `json:"IDs"`
}
type Thumbnail struct {
URL string `json:"Url"`
Height int `json:"Height"`
Width int `json:"Width"`
}
var obj = ComplexObject{
Image: Image{
Width: 1234,
Height: 4567,
Title: "This is an image",
Thumbnail: Thumbnail{
URL: "https://something.foo.bar",
Height: 8932,
Width: 12340,
},
Animated: true,
IDs: []int{
12,
34,
56,
78,
90,
},
},
}
func BenchmarkJSONEncode(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := json.Marshal(obj)
if err != nil {
b.Fatalf("Failed to marshal object as JSON: %v", err)
}
}
}
func BenchmarkJSONEncodeWithPrettyPrinting(b *testing.B) {
for i := 0; i < b.N; i++ {
// two space indent
_, err := json.MarshalIndent(obj, "", " ")
if err != nil {
b.Fatalf("Failed to marshal object as JSON: %v", err)
}
}
}
From here, if we then run this, we see:
% go test -bench=.
goos: linux
goarch: amd64
pkg: x
cpu: Intel(R) Core(TM) i7-5960X CPU @ 3.00GHz
BenchmarkJSONEncode-16 1000000 1767 ns/op
BenchmarkJSONEncodeWithPrettyPrinting-16 154684 6786 ns/op
PASS
ok x 2.915s
Notice that this takes 3 times longer to pretty-print the JSON. That's a significant impact!
Although it may not seem huge, and yes we're still at the nanoseconds, but it really adds up as your API gets used by more and more traffic.
Now, sometimes you may want to do the work for your users, especially as you should be working to avoid your users using any old online JSON pretty-printer, so something you could do is have a ?pretty=true
query parameter that can pretty-print the response where requested.
I'd still say really try and avoid it, as your consumers should be empowered to do the JSON pretty printing as-and-when they need it, but I guess it's your call.
This very likely won't be your service's massive bottleneck, but it'll definitely add up, and why not remove unnecessary work your service is doing, wasting resources + unnecessary network traffic?