Home > Back-end >  Unmarshaling from JSON key containing a single quote
Unmarshaling from JSON key containing a single quote

Time:12-10

I feel quite puzzled by this. I need to load some data (coming from a French database) that is serialized in JSON and in which some keys have a single quote.

Here is a simplified version:

package main

import (
    "encoding/json"
    "fmt"
)

type Product struct {
    Name string `json:"nom"`
    Cost int64  `json:"prix d'achat"`
}

func main() {
    var p Product
    err := json.Unmarshal([]byte(`{"nom":"savon", "prix d'achat": 170}`), &p)
    fmt.Printf("product cost: %d\nerror: %s\n", p.Cost, err)
}

// product cost: 0
// error: %!s(<nil>)

Unmarshaling leads to no errors however the "prix d'achat" (p.Cost) is not correctly parsed.

When I unmarshal into a map[string]any, the "prix d'achat" key is parsed as I would expect:

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    blob := map[string]any{}
    err := json.Unmarshal([]byte(`{"nom":"savon", "prix d'achat": 170}`), &blob)
    fmt.Printf("blob: %f\nerror: %s\n", blob["prix d'achat"], err)
}

// blob: 170.000000
// error: %!s(<nil>)

I checked the json.Marshal documentation on struct tags and I cannot find any issue with the data I'm trying to process.

Am I missing something obvious here? How can I parse a JSON key containing a single quote using struct tags?

Thanks a lot for any insight!

CodePudding user response:

I didn't find anything in the documentation, but the JSON encoder considers single quote to be a reserved character in tag names.

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
}

I think opening an issue is justified here. In the meantime, you're going to have to implement json.Unmarshaler and/or json.Marshaler. Here is a start:

func (p *Product) UnmarshalJSON(b []byte) error {
    type product Product // revent recursion
    var _p product

    if err := json.Unmarshal(b, &_p); err != nil {
        return err
    }

    *p = Product(_p)

    return unmarshalFieldsWithSingleQuotes(p, b)
}

func unmarshalFieldsWithSingleQuotes(dest interface{}, b []byte) error {
    // Look through the JSON tags. If there is one containing single quotes,
    // unmarshal b again, into a map this time. Then unmarshal the value
    // at the map key corresponding to the tag, if any.
    var m map[string]json.RawMessage

    t := reflect.TypeOf(dest).Elem()
    v := reflect.ValueOf(dest).Elem()

    for i := 0; i < t.NumField(); i   {
        tag := t.Field(i).Tag.Get("json")
        if !strings.Contains(tag, "'") {
            continue
        }

        if m == nil {
            if err := json.Unmarshal(b, &m); err != nil {
                return err
            }
        }

        if j, ok := m[tag]; ok {
            if err := json.Unmarshal(j, v.Field(i).Addr().Interface()); err != nil {
                return err
            }
        }
    }

    return nil
}

Try it on the playground: https://go.dev/play/p/aupACXorjOO

  • Related