对于encoding / json,如何优雅地处理在键名中包含撇号的API JSON调用?

I am writing a simple Go program to consume a simple API. Some values are not correctly unmarshaling into my struct, and I have traced the issue to the invalid key names in the returned JSON object.

I can reproduce the issue with this code:

jsonStr := `{
    "valid_json": "I'm Valid",
    "invalid'json": "I should be valid, but I'm not"
}`

type result struct {
    Valid   string `json:"valid_json"`
    Invalid string `json:"invalid'json"`
}

var res result
err := json.Unmarshal([]byte(jsonStr), &res)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Valid:   %s
", res.Valid)
fmt.Printf("Invalid: %s
", res.Invalid)

The resulting output:

Valid:   I'm Valid
Invalid: 

My expected output:

Valid:   I'm Valid
Invalid: I should be valid, but I'm not

I've tried options such as escaping the ' in the struct tag, but this either results in an error or it is just ignored. I've also looked into alternate methods but have come back empty-handed.

How would this issue be properly handled on my end? Would it be better to strip the ' before unmarshaling? Or is there some other way that I could accept the single quote?

According to the docs for json.Marshal...

The key name will be used if it's a non-empty string consisting of only Unicode letters, digits, and ASCII punctuation except quotation marks, backslash, and comma.

The relevant code appears to be isValidTag. According to the comment, "quote chars are reserved" for future use in the tag syntax.

func isValidTag(s string) bool {
    if s == "" {
        return false
    }
    for _, c := range s {
        switch {
        case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
            // Backslash and quote chars are reserved, but
            // otherwise any punctuation chars are allowed
            // in a tag name.
        case !unicode.IsLetter(c) && !unicode.IsDigit(c):
            return false
        }
    }
    return true
}

You can work around this by using an interface instead of a struct.

package main
import (
    "fmt"
    "encoding/json"
    "log"
)
func main() {
    jsonStr := `{
        "valid_json": "I'm Valid",
        "invalid'json": "I should be valid, but I'm not"
    }`

    var res interface{}
    err := json.Unmarshal([]byte(jsonStr), &res)
    if err != nil {
        log.Fatal(err)
    }

    m := res.(map[string]interface{})
    for k, v := range m {
        switch vv := v.(type) {
        case string:
            fmt.Println(k, "is string", vv)
        case float64:
            fmt.Println(k, "is float64", vv)
        case []interface{}:
            fmt.Println(k, "is an array:")
            for i, u := range vv {
                fmt.Println(i, u)
            }
        default:
            fmt.Println(k, "is of a type I don't know how to handle")
        }
    }
}

See "Decoding arbitrary data" in JSON and Go for more.