encoding/json/v2 package
The encoding/json/v2 package release date is yet to be determined, but I think it’s a good time to start thinking about encoding/json/v2 package and its impact on gqlgen.
The encoding/json/v2 package is planned to provide functionality compatible with the encoding/json package, and it seems that the internals of the existing encoding/json package will be built using the encoding/json/v2 package.
This means that gqlgen can benefit from performance improvements without requiring any changes. So far, there are no issues.
The problem arises when gqlgen directly uses the encoding/json/v2 package. I would like to clarify the benefits and issues that may occur in that case.
Reference
https://github.com/golang/go/issues/71497 https://pkg.go.dev/github.com/go-json-experiment/json
Additional Information (edit: added by Steve Coffman)
In #2842 there was a proposal to allow a pluggable JSON implementation for gqlgen. Currently the JSON marshalling cannot be easily replaced with a different implementation such asencoding/json/v2 (or other alternatives). We would like to be able to allow this to be a choice.
As in that proposal, we would add to gqlgen an API that accepts a custom implementation supplied directly to the transport at fields:
type Decoder interface {
Decode(v interface{}) error
UseNumber()
}
type Json interface {
Marshal(v interface{}) ([]byte, error)
NewDecoder(r io.Reader) Decoder
}
type POST struct {
// Map of all headers that are added to graphql response. If not
// set, only one header: Content-Type: application/json will be set.
ResponseHeaders map[string][]string
Json Json // Json being an interface so it can be implemented however the user wants
}
The library can then supply a default implementation based on encoding/json:
var DefaultJson Json = jsonImpl{}
type jsonImpl struct{}
func (jsonImpl) Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
func (jsonImpl) NewDecoder(r io.Reader) Decoder {
return json.NewDecoder(r)
}
Since there already exists a file that handles most JSON marshalling, it can be updated to accept that JSON implementation and fallback if one is not supplied:
func writeJson(json Json, w io.Writer, response *graphql.Response) {
if json == nil {
json = DefaultJson
}
b, err := json.Marshal(response)
if err != nil {
panic(err)
}
w.Write(b)
}
This will add a slight overhead of checking for the implementation, but will not break existing codebases.
This will also allow us to easily add a go version build tag when encoding/json/v2 is released, such that we can immediately upgrade to it if that Go version is available.
@sonatard I hope you don't mind, but I edited your issue and added some information from #2842 as I think it is a viable means of allowing people to opt-in to encoding/json/v2 even today for those who want to experiment with it even while it is an unstable API, and then later when it is released as part of the standard library.
Also relevant is #3161 and https://github.com/go-json-experiment/jsonbench
Here's my opinion: I think it would be better to proceed with applying the json v2 package before making it customizable. The reason is that the json v2 package introduces several new interfaces. I believe that understanding how to use v2 with gqlgen first, and then considering interfaces for opt-in customization, would lead to designing better interfaces. Additionally, I think third-party json packages should also revise their interfaces with reference to v2. Changes to the interfaces, such as directly accepting io.Writer, bring performance advantages.
Moreover, since json v2 significantly improves performance, I believe the demand for opting into alternative json packages will likely decrease.
I think you're right, but my concern is that we are stuck waiting for a year or more for this to be released in a Go version, then for Google App Engine to eventually adopt it and they sometimes take a really long time.
I don't propose that we add a permanent configuration option. I'm suggesting we temporarily make a transparent implementation detail that allows us the flexibility to begin experimenting with both the performance improvements using the current interface. At the same time, we can experiment with build flag guarded new interfaces that might lead to a much better design over time. After encoding/json/v2 is in the standard version of Go that is in a released version of Go that is not only "the one behind latest" according to the Go security policy, but it is also supported by Google AppEngine as Generally Available, then we can remove any lingering references to encoding/json.
What do you think of that?
Backward compatibility is important, I was also a Google App Engine user, and it was truly amazing. (On a side note, I'm now using Cloud Run, and I love its fast local builds with ko and low cost operation.)
I think it’s a good idea to design an interface based on the v2 package and make it possible to switch between v1 and v2 packages according to that interface.
1.Consideration will be given to integrating the v2 package without merging it into the master branch. The interface will be evaluated. Confirmation will be made that the v1 package can be used with this interface. 2. The interface will be released, setting the v1 package as the default. The v2 package will be made available as an option (whether build tags are necessary will require further consideration).
This approach maintains backward compatibility while requiring maintenance of only one well-designed opt-in interface, reducing maintenance costs.
The disadvantage of this approach is the delayed provision of the opt-in interface, but it is expected to increase development speed in the long term.
If I got to pick, we would have switched off of GAE to Cloud Run and/or GKE a long time ago! GAE was an amazing step forward when it came out. I use ko for all my stuff on GKE and absolutely love it! (Kinda bummed that I have to have a dockerfile for CloudRun v2).
I'm ok with your plan.
When migrating to jsonv2, there are a few points to be aware of.
In jsonv2, when marshaling a JSON null value into a non-pointer slice or map, it will result in an empty slice or an empty map.
This can be confusing, since you may have intended to pass null from GraphQL but it instead becomes empty. To address this, there are two approaches:
- Use the FormatNilSliceAsNull and FormatNilMapAsNull options.
- In gqlgen, generate optional array or map fields in GraphQL as pointer slices or maps in Go. This avoids the situation where a GraphQL null gets marshaled into a non-pointer slice or map as nil.
Reference: https://github.com/golang/go/discussions/63397#discussioncomment-7201222