go-json
go-json copied to clipboard
Interpret single JSON object as an array with one element?
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)
}
})
}
}
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.
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)
}
},
)
}
}