I want to unmarshal JSON, which dynamically changes shape depending on the tag, and keep the value.

Asked 1 years ago, Updated 1 years ago, 62 views

Consider JSON, which has a rule that the shape of other keys changes according to the value of a particular key in JSON.For example:

Externally Tagged

[
    {"RGB":{"R":98, "G":218, "B":255}},
    {"YCbCr":{"Y":255, "Cb":0, "Cr":-10}}
]

Internal Tagged

[
    {"Space": "RGB", "R":98, "G":218, "B":255},
    {"Space": "YCbCr", "Y": 255, "Cb": 0, "Cr":-10}
]

Adjacently Tagged

[
    {"Space": "RGB", "Point": {"R":98, "G":218, "B":255}},
    {"Space": "YCbCr", "Point": {"Y":255, "Cb":0, "Cr":-10}}
]

*The term "○○Tagged" was borrowed from Rust's serde document.

Thus, the main question is to unmarshal JSON with Go, which depends on a specific value.

One such mechanism for unmarshal of JSON is that encoding/json has the type json.RawMessage.This is supposed to be used to delay unmarshal by keeping the raw string before unmarshal, and unmarshal if necessary when using it.

If Adjacently tagged, the specific source code is Tag and subsequent RawMessage.

In the case of Internally tagged, for example, you can write the flow of unmarshal only once, switch to that value, and unmarshal the whole thing again.The sample code looks like this.It's a little awkward to unmarshal the whole thing twice.

The hard part of using json.RawMessage is that whenever you use it as a RawMessage, you need to unmarshal it.If possible, I would like to write it so that I can use the data around without unmarshal once.

Existing stack overflow questions also found unmarshal as a map.

However, if you accept it as a map, you will not benefit from the system:

  • Even if there is an incorrect value, it cannot be played at unmarshal.For example, unmarshal as map[string]interface{} does not cause errors if int is written in the key where string is expected. Unmarshal as map[string]string does not support keys other than string, such as int and structure.
  • Also, depending on the type, it is difficult to manage the shape of JSON in one place.

For the same reason, unmarshal as interface{} and type assertions when used are a bit tricky.

Map and interface{} are convenient when you want to unmarshal in a loose and fluffy way, but I would like to take a closer look at them this time.In this direction, libraries dproxy and jsonpointer are known to be useful.

Is there a way to unmarshal JSON that dynamically changes shape depending on the tag?

It is better not to unmarshal every time you use it, but to unmarshal once you use it, you don't have to unmarshal after that.Also, I would like to benefit from the type as much as possible, and I would appreciate it if you could find out the error at the first unmarshal.

Environment: Go 1.14

go json

2022-09-30 14:35

1 Answers

I found a way to use the UnmarshalJSON method.The type with this method is treated as json.Unmarshaler and can be used by json.Unmarshal.

Sample Code for Internally tagged:

type Color structure {
    Space string
    Content interface {}
}
type RGB structure {
    Ruint 8
    Guint 8
    Buint 8
}
US>type YCbCrruct{
    Youint8
    Cbint8
    Crint 8
}

func(c*Color)UnmarshalJSON(data[]byte)error{
    // By convention, Unmarshals implement UnmarshalJSON([]byte("null") asano-op.
    // TODO: ↑ Is this the correct interpretation of this sentence?
    if bytes.Equal(data, []byte("null")){
        return nil
    }

    varspace=struct{
        Space string
    }{}
    err —=json.Unmarshal (data, & space)
    if err!=nil{
        return fmt.Errorf("Space not found: %w", error)
    }
    c.Space=space.Space

    switchspace.Space{
    case "RGB":
        varrgb RGB
        iferr:=json.Unmarshal(data, & rgb);err!=nil{
            return fmt.Errorf("Space says this is RGB, but cannot unmarshal as RGB: %w", err)
        }
        c. Content=rgb
    case "YCbCr":
        varycbcr YCbCr
        iferr:=json.Unmarshal(data, & ycbcr);err!=nil{
            return fmt.Errorf("Space says this is YCbCr, but cannot unmarshal as YCbCr: %w", error)
        }
        c. Content=ycbcr
    default:
        return errors.New("Unknown Space:"+space.Space")
    }

    return nil
}

funcmain(){
    varj=[]byte(`[
        {"Space": "YCbCr", "Y": 255, "Cb": 0, "Cr":-10},
        {"Space": "RGB", "R":98, "G":218, "B":255}
    ]`)
    var colors [ ]Color
    err: = json.Unmarshal(j, & colors)
    if err!=nil{
        log.Fatal(err)
    }

    for_,c:=range colors{
        switch c.Space {
        case "RGB":
            rgb,_ —=c.Content. (RGB)
            fmt.Println(c.Space,rgb)
        case "YCbCr":
            ycbcr,_ —=c.Content.(YCbCr)
            fmt.Println(c.Space, ycbcr)
        }
    }
}

Now unmarshal is once.

The disadvantages are that we compare tags with string, and that we must make a successful assertion (which is practically tagged union).I would appreciate it if you could let me know if there is any way I can do anything about this area in the comments or in a separate answer.Do you think you'll do well with go generate...?


2022-09-30 14:35

If you have any answers or tips


© 2024 OneMinuteCode. All rights reserved.