goccy / go-json

Fast JSON encoder/decoder compatible with encoding/json for Go
MIT License
2.87k stars 139 forks source link

Interpret single JSON object as an array with one element? #388

Open cxjava opened 1 year ago

cxjava commented 1 year ago

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)
            }
        })
    }
}
ochiama commented 1 year ago

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.

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.

kislerdm commented 1 year ago

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)
                }
            },
        )
    }
}