Skip to content

Conversation

goccy
Copy link
Owner

@goccy goccy commented Jan 2, 2022

What

In encoding, you can dynamically filter the fields in the structure by creating a json.FieldQuery, adding it to context.Context using json.SetFieldQueryToContext and then passing it to json.MarshalContext.
This is a type-safe operation, so it is faster than filtering using map[string]interface{} .

Motivation

When providing the REST API or GraphQL API, you may want to select and encode the fields of a structure.
In such a case, the conventional method requires to prepare a value of type map[string]interface{}, transfer the value of the selected field to map once, and then encode it.
This process is not only costly to create a map, but also a slow operation because you can't benefit from static typing.
So in go-json, if you creates json.FieldQuery that has information about selected fields and set it in context.Context and pass it to encoder,
it will dynamically filter according to the contents of json.FieldQuery.
By supporting this by the encoder itself, it can be operated at high speed.
You can also apply the filter when writing arbitrary processing in MarshalJSON by combining it with MarshalJSON(context.Context)([]byte, error).

See examples

Example1

package json_test

import (
	"context"
	"reflect"
	"testing"

	"github.com/goccy/go-json"
)

type queryTestX struct {
	XA int
	XB string
	XC *queryTestY
	XD bool
	XE float32
}

type queryTestY struct {
	YA int
	YB string
	YC *queryTestZ
	YD bool
	YE float32
}

type queryTestZ struct {
	ZA string
	ZB bool
	ZC int
}

func (z *queryTestZ) MarshalJSON(ctx context.Context) ([]byte, error) {
	type _queryTestZ queryTestZ
	return json.MarshalContext(ctx, (*_queryTestZ)(z))
}

func TestFieldQuery(t *testing.T) {
	query, err := json.BuildFieldQuery(
		"XA",
		"XB",
		json.BuildSubFieldQuery("XC").Fields(
			"YA",
			"YB",
			json.BuildSubFieldQuery("YC").Fields(
				"ZA",
				"ZB",
			),
		),
	)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(query, &json.FieldQuery{
		Fields: []*json.FieldQuery{
			{
				Name: "XA",
			},
			{
				Name: "XB",
			},
			{
				Name: "XC",
				Fields: []*json.FieldQuery{
					{
						Name: "YA",
					},
					{
						Name: "YB",
					},
					{
						Name: "YC",
						Fields: []*json.FieldQuery{
							{
								Name: "ZA",
							},
							{
								Name: "ZB",
							},
						},
					},
				},
			},
		},
	}) {
		t.Fatal("cannot get query")
	}
	queryStr, err := query.QueryString()
	if err != nil {
		t.Fatal(err)
	}
	if queryStr != `["XA","XB",{"XC":["YA","YB",{"YC":["ZA","ZB"]}]}]` {
		t.Fatalf("failed to create query string. %s", queryStr)
	}
	ctx := json.SetFieldQueryToContext(context.Background(), query)
	b, err := json.MarshalContext(ctx, &queryTestX{
		XA: 1,
		XB: "xb",
		XC: &queryTestY{
			YA: 2,
			YB: "yb",
			YC: &queryTestZ{
				ZA: "za",
				ZB: true,
				ZC: 3,
			},
			YD: true,
			YE: 4,
		},
		XD: true,
		XE: 5,
	})
	if err != nil {
		t.Fatal(err)
	}
	expected := `{"XA":1,"XB":"xb","XC":{"YA":2,"YB":"yb","YC":{"ZA":"za","ZB":true}}}`
	got := string(b)
	if expected != got {
		t.Fatalf("failed to encode with field query: expected %q but got %q", expected, got)
	}
}

Example2

package json_test

import (
	"context"
	"fmt"
	"log"

	"github.com/goccy/go-json"
)

type User struct {
	ID      int64
	Name    string
	Age     int
	Address UserAddressResolver
}

type UserAddress struct {
	UserID   int64
	PostCode string
	City     string
	Address1 string
	Address2 string
}

type UserRepository struct {
	uaRepo *UserAddressRepository
}

func NewUserRepository() *UserRepository {
	return &UserRepository{
		uaRepo: NewUserAddressRepository(),
	}
}

type UserAddressRepository struct{}

func NewUserAddressRepository() *UserAddressRepository {
	return &UserAddressRepository{}
}

type UserAddressResolver func(context.Context) (*UserAddress, error)

func (resolver UserAddressResolver) MarshalJSON(ctx context.Context) ([]byte, error) {
	address, err := resolver(ctx)
	if err != nil {
		return nil, err
	}
	return json.MarshalContext(ctx, address)
}

func (r *UserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
	user := &User{ID: id, Name: "Ken", Age: 20}
	// resolve relation from User to UserAddress
	user.Address = func(ctx context.Context) (*UserAddress, error) {
		return r.uaRepo.FindByUserID(ctx, user.ID)
	}
	return user, nil
}

func (*UserAddressRepository) FindByUserID(ctx context.Context, id int64) (*UserAddress, error) {
	return &UserAddress{
		UserID:   id,
		City:     "A",
		Address1: "foo",
		Address2: "bar",
	}, nil
}

func Example_fieldQuery() {
	ctx := context.Background()
	userRepo := NewUserRepository()
	user, err := userRepo.FindByID(ctx, 1)
	if err != nil {
		log.Fatal(err)
	}
	query, err := json.BuildFieldQuery(
		"Name",
		"Age",
		json.BuildSubFieldQuery("Address").Fields(
			"City",
		),
	)
	if err != nil {
		log.Fatal(err)
	}
	ctx = json.SetFieldQueryToContext(ctx, query)
	b, err := json.MarshalContext(ctx, user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(b))

	// Output:
	// {"Name":"Ken","Age":20,"Address":{"City":"A"}}
}

Benchmark

$ cd benchmarks && go test -bench Encode_FilterBy
goos: darwin
goarch: amd64
pkg: benchmark
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
Benchmark_Encode_FilterByMap-16                   852588              1252 ns/op             898 B/op          8 allocs/op
Benchmark_Encode_FilterByFieldQuery-16           7513506               156.5 ns/op            48 B/op          1 allocs/op
PASS
ok      benchmark       2.608s

@codecov-commenter
Copy link

codecov-commenter commented Jan 2, 2022

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

Attention: Patch coverage is 67.72908% with 81 lines in your changes missing coverage. Please review.

Project coverage is 79.81%. Comparing base (b0f4ac6) to head (594d0a5).
Report is 139 commits behind head on master.

❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #314      +/-   ##
==========================================
- Coverage   79.97%   79.81%   -0.17%     
==========================================
  Files          49       51       +2     
  Lines       17231    17472     +241     
==========================================
+ Hits        13781    13945     +164     
- Misses       2900     2962      +62     
- Partials      550      565      +15     

@goccy goccy force-pushed the feature/json-field-query branch from 1d25161 to 89bcc3b Compare January 3, 2022 03:34
@goccy goccy merged commit 0707c2a into master Jan 3, 2022
@goccy goccy deleted the feature/json-field-query branch January 3, 2022 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants