go-json icon indicating copy to clipboard operation
go-json copied to clipboard

Interpret single JSON object as an array with one element?

Open cxjava opened this issue 3 years ago • 3 comments

Hi, I want to handle the json format "address":["road1","road2"] and "address":"road1" using one struct Address []string. In Java, some library have this ability to handle this special case, like https://stackoverflow.com/questions/17003823/make-jackson-interpret-single-json-object-as-array-with-one-element

Can we add this feature? or do we already have this feature? I am newbie of this lib.

JSON format A: {"name":"xiao","address":["road1","road2"]} JSON format B: {"name":"xiao","address":"road1"}

struct:

struct {
   Name    string
   Address []string // expected: []string
}

Test code:

package main

import (
	"fmt"
	"testing"

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

func Test_gojson(t *testing.T) {
	type args struct {
		jsonStr string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		{
			name: "gojson with list",
			args: args{
				`{"name":"xiao","address":["road1","road2"]}`,
			},
			want: `{Name:xiao Address:[road1 road2]}`,
		},
		{
			name: "gojson with one element in array",
			args: args{
				`{"name":"xiao","address":"road1"}`,
			},
			want: `{Name:xiao Address:[]}`, // expected: `{Name:xiao Address:[road1]}`
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {

			o := struct {
				Name    string
				Address []string // expected: []string
			}{}

			_ = gojson.Unmarshal([]byte(tt.args.jsonStr), &o)

			if got := fmt.Sprintf("%+v", o); got != tt.want {
				t.Errorf("gojson.Unmarshal() = %v, want %v", got, tt.want)
			}
		})
	}
}

cxjava avatar Aug 01 '22 10:08 cxjava

Hello, I am very new to Go and I just started to read and learn from documentation and OSS. I apologize in advance if I come up with silly misconceptions.

In another library, I read about how they deal with arbitrary JSON content i.e.

But what if you don’t know the structure of your JSON data beforehand?

The suggestion was the use of empty interface interface{} i.e.

  • map[string]interface{}
  • []interface{}

If it isn't supported yet with this, it may be a good generic structure.

https://go.dev/blog/json in section Generic JSON with interface.

ochiama avatar Aug 03 '22 07:08 ochiama

Hey! What you propose goes against best practice of single responsibility for a unit of logic. The unmarshal method must carry the single responsibility of deserializing bytes into a Go object only. On that basis, IMO the request shall be discarded.

Provided example indicates that the input data structures have different format, ergo different Go type. Decision about the type shall be taken after deserialization as part of your business logic.

Possible solution for your problem is a custom unmarshall for the destination interface. Find the execution of the example bellow.

package demo

import (
	"fmt"
	"reflect"
	"testing"

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

// your original struct
type obj struct {
	Name    string   `json:"name"`
	Address []string `json:"address"`
}

// extended original struct
// custom unmarshal logic was added
type objExt struct {
	Name    string   `json:"name"`
	Address []string `json:"address"`
}

func (o *objExt) UnmarshalJSON(data []byte) error {
	var tmp map[string]interface{}
	err := json.Unmarshal(data, &tmp)
	if err != nil {
		return err
	}

	address, ok := tmp["address"]
	if !ok {
		return fmt.Errorf("address field not found")
	}

	switch address.(type) {
	case []interface{}:
		for _, el := range address.([]interface{}) {
			o.Address = append(o.Address, el.(string))
		}
	case string:
		o.Address = []string{address.(string)}
	default:
		o.Address = nil
	}

	name, ok := tmp["name"]
	if !ok {
		return fmt.Errorf("name field not found")
	}
	o.Name = name.(string)

	return nil
}

func Test_demo(t *testing.T) {
	type args struct {
		data []byte
		v    interface{}
	}

	tests := []struct {
		name    string
		args    args
		want    interface{}
		wantErr bool
	}{
		{
			name: "happy path: array input for []string Go type, original Go struct",
			args: args{
				data: []byte(`{"name":"xiao","address":["road1","road2"]}`),
				v:    &obj{},
			},
			want: &obj{
				Name:    "xiao",
				Address: []string{"road1", "road2"},
			},
			wantErr: false,
		},
		{
			name: "unhappy path: string input for []string Go type, original Go struct",
			args: args{
				data: []byte(`{"name":"xiao","address":"road1"}`),
				v:    &obj{},
			},
			want: &obj{
				Name: "xiao",
			},
			wantErr: true,
		},
		{
			name: "happy path: array input for []string Go type, extended Go struct",
			args: args{
				data: []byte(`{"name":"xiao","address":["road1","road2"]}`),
				v:    &objExt{},
			},
			want: &objExt{
				Name:    "xiao",
				Address: []string{"road1", "road2"},
			},
			wantErr: false,
		},
		{
			name: "happy path: string input for []string Go type, extended Go struct",
			args: args{
				data: []byte(`{"name":"xiao","address":"road1"}`),
				v:    &objExt{},
			},
			want: &objExt{
				Name:    "xiao",
				Address: []string{"road1"},
			},
			wantErr: false,
		},
	}
	for _, tt := range tests {
		t.Run(
			tt.name, func(t *testing.T) {
				if err := json.Unmarshal(tt.args.data, tt.args.v); (err != nil) != tt.wantErr {
					t.Errorf("unmarshal() error = %v, wantErr %v", err, tt.wantErr)
				}
				if !reflect.DeepEqual(tt.want, tt.args.v) {
					t.Errorf("unmarshal() got = %v, want %v", tt.args.v, tt.want)
				}
			},
		)
	}
}

kislerdm avatar Aug 06 '22 11:08 kislerdm