How to parse both plain and enquoted JSON numbers in Go? - json

I'm dealing with a third-party JSON-based API. Usually it wraps all numbers in quotes, but sometimes it doesn't. Nothing I can do about it.
I'm trying to come up with a solution, which parses the numbers regardless of whether they're enquoted or not. Standard library provides a ,string field tag, which allows to map numeric fields to enquoted values, but, unfortunately, it then fails to process the value, if it's not in quotes.
func test(s string) {
err := json.Unmarshal([]byte(s), &struct {
F1 float64
F2 float64 `json:",string"`
}{})
if err != nil {
fmt.Println(err)
return
}
fmt.Println("success")
}
func main() {
test(`{"f1": 1.23}`) // success
test(`{"f1": "1.23"}`) // cannot unmarshal string into Go value of type float64
test(`{"f2": 1.23}`) // invalid use of ,string struct tag, trying to unmarshal unquoted value into ...
test(`{"f2": "1.23"}`) // success
}
The Go Playground
Is there a way around this?

The proper solution is to "clone" float64 and define custom MarshalJSON and UnmarshalJSON for it:
type jsonFloat64 float64
func (f jsonFloat64) MarshalJSON() ([]byte, error) {
return json.Marshal(float64(f))
}
func (f *jsonFloat64) UnmarshalJSON(data []byte) error {
if len(data) >= 2 && data[0] == '"' && data[len(data)-1] == '"' {
data = data[1 : len(data)-1]
}
var tmp float64
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
*f = jsonFloat64(tmp)
return nil
}
Then you'd be able to do something like this:
func test(s string) {
err := json.Unmarshal([]byte(s), &struct {
F jsonFloat64
}{})
if err != nil {
fmt.Println(err)
return
}
fmt.Println("success")
}
func main() {
test(`{"f": 1.23}`) // success
test(`{"f": "1.23"}`) // success
}
The Go Playground
Feel free to adjust UnmarshalJSON to your needs, mine is pretty strict about the spacing. Credit goes to Dave C, this was heavily inspired by his comment on another question (which also features more variations on the solution above).
Alternatively, you could pre-process the JSON data with a regular expression, but don't do this if the solution above is a viable option, it's much faster.
re := regexp.MustCompile(`(":\s*)([\d\.]+)(\s*[,}])`)
rawJsonByteArray = re.ReplaceAll(rawJsonByteArray, []byte(`$1"$2"$3`))

Related

Custom unmarshaling a struct into a map of slices

I thought I understood unmarshalling by now, but I guess not. I'm having a little bit of trouble unmarshalling a map in go. Here is the code that I have so far
type OHLC_RESS struct {
Pair map[string][]Candles
Last int64 `json:"last"`
}
type Candles struct {
Time uint64
Open string
High string
Low string
Close string
VWAP string
Volume string
Count int
}
func (c *Candles) UnmarshalJSON(d []byte) error {
tmp := []interface{}{&c.Time, &c.Open, &c.High, &c.Low, &c.Close, &c.VWAP, &c.Volume, &c.Count}
length := len(tmp)
err := json.Unmarshal(d, &tmp)
if err != nil {
return err
}
g := len(tmp)
if g != length {
return fmt.Errorf("Lengths don't match: %d != %d", g, length)
}
return nil
}
func main() {
response := []byte(`{"XXBTZUSD":[[1616662740,"52591.9","52599.9","52591.8","52599.9","52599.1","0.11091626",5],[1616662740,"52591.9","52599.9","52591.8","52599.9","52599.1","0.11091626",5]],"last":15}`)
var resp OHLC_RESS
err := json.Unmarshal(response, &resp)
fmt.Println("resp: ", resp)
}
after running the code, the last field will unmarshal fine, but for whatever reason, the map is left without any value. Any help?
The expedient solution, for the specific example JSON, would be to NOT use a map at all but instead change the structure of OHLC_RESS so that it matches the structure of the JSON, i.e.
type OHLC_RESS struct {
Pair []Candles `json:"XXBTZUSD"`
Last int64 `json:"last"`
}
https://go.dev/play/p/Z9PhJt3wX33
However it's safe to assume, I think, that the reason you've opted to use a map is because the JSON object's key(s) that hold the "pairs" can vary and so hardcoding them into the field's tag is out of the question.
To understand why your code doesn't produce the desired result, you have to realize two things. First, the order of a struct's fields has no bearing on how the keys of a JSON object will be decoded. Second, the name Pair holds no special meaning for the unmarshaler. Therefore, by default, the unmarshaler has no way of knowing that your wish is to decode the "XXBTZUSD": [ ... ] element into the Pair map.
So, to get your desired result, you can have the OHLC_RESS implement the json.Unmarshaler interface and do the following:
func (r *OHLC_RESS) UnmarshalJSON(d []byte) error {
// first, decode just the object's keys and leave
// the values as raw, non-decoded JSON
var obj map[string]json.RawMessage
if err := json.Unmarshal(d, &obj); err != nil {
return err
}
// next, look up the "last" element's raw, non-decoded value
// and, if it is present, then decode it into the Last field
if last, ok := obj["last"]; ok {
if err := json.Unmarshal(last, &r.Last); err != nil {
return err
}
// remove the element so it's not in
// the way when decoding the rest below
delete(obj, "last")
}
// finally, decode the rest of the element values
// in the object and store them in the Pair field
r.Pair = make(map[string][]Candles, len(obj))
for key, val := range obj {
cc := []Candles{}
if err := json.Unmarshal(val, &cc); err != nil {
return err
}
r.Pair[key] = cc
}
return nil
}
https://go.dev/play/p/Lj8a8Gx9fWH

Handling different types when unmarshalling a json [duplicate]

This question already has an answer here:
unmarshal nested json without knowing structure
(1 answer)
Closed 3 years ago.
I'm consuming an endpoint (which I don't own and I cannot fix) and this endpoint returns JSON.
The problem is this JSON can come in different formats:
Format 1:
{
"message": "Message"
}
or
{
"message": ["ERROR_CODE"]
}
Depending on what happened.
I'd like to have one struct to hold this response so later I can check if the message is a string or array, and properly follow a flow.
Is it possible to do it in Go? The first approach I thought was to have two structs and try to decode to the one with string, and if an error happens, try to decode to the one with array.
Is there a more elegant approach?
Unmarshal it into a value of interface{} type, and use a type assertion or type switch to inspect the type of value it ends up. Note that by default JSON arrays are unmarshaled into a value of type []interface{}, so you have to check for that to detect the error response.
For example:
type Response struct {
Message interface{} `json:"message"`
}
func main() {
inputs := []string{
`{"message":"Message"}`,
`{"message":["ERROR_CODE"]}`,
}
for _, input := range inputs {
var r Response
if err := json.Unmarshal([]byte(input), &r); err != nil {
panic(err)
}
switch x := r.Message.(type) {
case string:
fmt.Println("Success, message:", x)
case []interface{}:
fmt.Println("Error, code:", x)
default:
fmt.Println("Something else:", x)
}
}
}
Output (try it on the Go Playground):
Success, message: Message
Error, code: [ERROR_CODE]
You can use a custom type that implements json.Unmarshaller and tries to decode each possible input format in turn:
type Message struct {
Text string
Codes []int
}
func (m *Message) UnmarshalJSON(input []byte) error {
var text string
err := json.Unmarshal(input, &text)
if err == nil {
m.Text = text
m.Codes = nil
return nil
}
var codes []int
err := json.Unmarshal(input, &codes)
if err == nil {
m.Text = nil
m.Codes = codes
return nil
}
return err
}
I prefer this approach over unmarshalling into interface{} and type-asserting later because all the type checks are encapsulated in the unmarshalling step.
For a real-world example, check out my type veryFlexibleUint64: https://github.com/sapcc/limes/blob/fb212143c5f5b3e9272994872fcc7b758ae47646/pkg/plugins/client_ironic.go
I would suggest creating a different type for Message and making that implement json.Unmarshaller
here is how the code would be
type message struct {
Text string
Codes []string //or int , assuming array of string as it was not mentioned in the question
}
func (m *message) UnmarshalJSON(input []byte) error {
if len(input) == 0 {
return nil
}
switch input[0] {
case '"':
m.Text = strings.Trim(string(input), `"`)
return nil
case '[':
return json.Unmarshal(input, &m.Codes)
default:
return fmt.Errorf(`invalid character %q looking for " or [`, input[0])
}
}
type Error struct {
Message message `json:"message"`
}
You may find full code with test here

Stop json.Marshal() from stripping trailing zero from floating point number

I got the following problem:
My golang program converts some information into JSON.
For example it results in the following json:
{
"value":40,
"unit":"some_string"
}
The problem is the "input" for value is 40.0 and the marshalling strips the trailing zero. It would be no problem if the EPL which reads the JSON would be able to read 40 as float without the .0
So the JSON output should look like:
{
"value":40.0,
"unit":"some_string"
}
Is there a possibility to "stop" json.Marshal() from removing the zero?
Edit: Value must be a Float
#icza provided a good answer, but just to offer another option, you can define your own float type and define your own serialization for it. Like this
type KeepZero float64
func (f KeepZero) MarshalJSON() ([]byte, error) {
if float64(f) == float64(int(f)) {
return []byte(strconv.FormatFloat(float64(f), 'f', 1, 32)), nil
}
return []byte(strconv.FormatFloat(float64(f), 'f', -1, 32)), nil
}
type Pt struct {
Value KeepZero
Unit string
}
func main() {
data, err := json.Marshal(Pt{40.0, "some_string"})
fmt.Println(string(data), err)
}
This results in {"Value":40.0,"Unit":"some_string"} <nil>. Check it out in playground.
By default floating point numbers are rendered without a decimal point and fractions if its value is an integer value. The representation is shorter, and it means the same number.
If you want control over how a number appears in the JSON representation, use the json.Number type.
Example:
type Pt struct {
Value json.Number
Unit string
}
func main() {
data, err := json.Marshal(Pt{json.Number("40.0"), "some_string"})
fmt.Println(string(data), err)
}
Output (try it on the Go Playground):
{"Value":40.0,"Unit":"some_string"} <nil>
If you have a number as a float64 value, you may convert it to json.Number like this:
func toNumber(f float64) json.Number {
var s string
if f == float64(int64(f)) {
s = fmt.Sprintf("%.1f", f) // 1 decimal if integer
} else {
s = fmt.Sprint(f)
}
return json.Number(s)
}
Testing it:
f := 40.0
data, err := json.Marshal(Pt{toNumber(f), "some_string"})
fmt.Println(string(data), err)
f = 40.123
data, err = json.Marshal(Pt{toNumber(f), "some_string"})
fmt.Println(string(data), err)
Output (try it on the Go Playground):
{"Value":40.0,"Unit":"some_string"} <nil>
{"Value":40.123,"Unit":"some_string"} <nil>
The other direction, if you want the float64 value of a json.Number, simply call its Number.Float64() method.
I had a similar issue where I wanted to marshal a map[string]interface{} with float values f.x 1.0 to JSON as 1.0. I solved it by adding a custom Marshal function for a custom float type and then replace the floats in the map with the custom type:
type customFloat float64
func (f customFloat) MarshalJSON() ([]byte, error) {
if float64(f) == math.Trunc(float64(f)) {
return []byte(fmt.Sprintf("%.1f", f)), nil
}
return json.Marshal(float64(f))
}
func replaceFloat(value map[string]interface{}) {
for k, v := range value {
switch val := v.(type) {
case map[string]interface{}:
replaceFloat(val)
case float64:
value[k] = customFloat(val)
}
}
}
Then replace all float64 nodes:
replaceFloat(myValue)
bytes, err := json.Marshal(myValue)
This will print the floats like 1.0
type MyFloat float64
func (mf MyFloat) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%.1f", float64(mf))), nil
}
used like this
type PricePoint struct {
Price MyFloat `json:"price"`
From time.Time `json:"valid_from"`
To time.Time `json:"valid_to"`
}
Replace the 1 in "%.1f" with what ever precision you need
Save the value as a string and cast it back if you need it.

Golang: JSON: How do I unmarshal array of strings into []int64

Golang encoding/json package lets you use ,string struct tag in order to marshal/unmarshal string values (like "309230") into int64 field. Example:
Int64String int64 `json:",string"`
However, this doesn't work for slices, ie. []int64:
Int64Slice []int64 `json:",string"` // Doesn't work.
Is there any way to marshal/unmarshal JSON string arrays into []int64 field?
Quote from https://golang.org/pkg/encoding/json:
The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types. This extra level of encoding is sometimes used when communicating with JavaScript programs:
For anyone interested, I found a solution using a custom type having MarshalJSON() and UnmarshalJSON() methods defined.
type Int64StringSlice []int64
func (slice Int64StringSlice) MarshalJSON() ([]byte, error) {
values := make([]string, len(slice))
for i, value := range []int64(slice) {
values[i] = fmt.Sprintf(`"%v"`, value)
}
return []byte(fmt.Sprintf("[%v]", strings.Join(values, ","))), nil
}
func (slice *Int64StringSlice) UnmarshalJSON(b []byte) error {
// Try array of strings first.
var values []string
err := json.Unmarshal(b, &values)
if err != nil {
// Fall back to array of integers:
var values []int64
if err := json.Unmarshal(b, &values); err != nil {
return err
}
*slice = values
return nil
}
*slice = make([]int64, len(values))
for i, value := range values {
value, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return err
}
(*slice)[i] = value
}
return nil
}
The above solution marshals []int64 into JSON string array. Unmarshaling works from both JSON string and integer arrays, ie.:
{"bars": ["1729382256910270462", "309286902808622", "23"]}
{"bars": [1729382256910270462, 309286902808622, 23]}
See example at https://play.golang.org/p/BOqUBGR3DXm
As you quoted from json.Marshal(), the ,string option only applies to specific types, namely:
The "string" option signals that a field is stored as JSON inside a JSON-encoded string. It applies only to fields of string, floating point, integer, or boolean types.
You want it to work with a slice, but that is not supported by the json package.
If you still want this functionality, you have to write your custom marshaling / unmarshaling logic.
What you presented works, but it is unnecessarily complex. This is because you created your custom logic on slices, but you only want this functionality on individual elements of the slices (arrays). You don't want to change how an array / slice (as a sequence of elements) is rendered or parsed.
So a much simpler solution is to only create a custom "number" type producing this behavior, and elements of slices of this custom type will behave the same.
Our custom number type and the marshaling / unmarshaling logic:
type Int64Str int64
func (i Int64Str) MarshalJSON() ([]byte, error) {
return json.Marshal(strconv.FormatInt(int64(i), 10))
}
func (i *Int64Str) UnmarshalJSON(b []byte) error {
// Try string first
var s string
if err := json.Unmarshal(b, &s); err == nil {
value, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return err
}
*i = Int64Str(value)
return nil
}
// Fallback to number
return json.Unmarshal(b, (*int64)(i))
}
And that's all!
The type using it:
type Foo struct {
Bars []Int64Str `json:"bars"`
}
Testing it the same way as you did yields the same result. Try it on the Go Playground.

Custom marshalling to bson and JSON (Golang & mgo)

I have the following type in Golang:
type Base64Data []byte
In order to support unmarshalling a base64 encoded string to this type, I did the following:
func (b *Base64Data) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
return nil
}
content, err := base64.StdEncoding.DecodeString(string(data[1 : len(data)-1]))
if err != nil {
return err
}
*b = []byte(xml)
return nil
}
Now I also want to be able to marshal and unmarshal it to mongo database, using mgo Golang library.
The problem is that I already have documents there stored as base64 encoded string, so I have to maintain that.
I tried to do the following:
func (b Base64Data) GetBSON() (interface{}, error) {
return base64.StdEncoding.EncodeToString([]byte(b)), nil
}
func (b *Base64DecodedXml) SetBSON(raw bson.Raw) error {
var s string
var err error
if err = raw.Unmarshal(&s); err != nil {
return err
}
*b, err = base64.StdEncoding.DecodeString(s)
return err
}
So that after unmarshaling, the data is already decoded, so I need to encode it back, and return it as a string so it will be written to db as a string (and vice versa)
For that I implemented bson getter and setter, but it seems only the getter is working properly
JSON unmarshaling from base64 encoded string works, as well marshaling it to database. but unmarshling setter seems to not be called at all.
Can anyone suggest what I'm missing, so that I'll be able to properly hold the data decoded in memory, but encoded string type?
This is a test I tried to run:
b := struct {
Value shared.Base64Data `json:"value" bson:"value"`
}{}
s := `{"value": "PHJvb3Q+aGVsbG88L3Jvb3Q+"}`
require.NoError(t, json.Unmarshal([]byte(s), &b))
t.Logf("%v", string(b.Value))
b4, err := bson.Marshal(b)
require.NoError(t, err)
t.Logf("%v", string(b4))
require.NoError(t, bson.Unmarshal(b4, &b))
t.Logf("%v", string(b.Value))
You can't marshal any value with bson.Marshal(), only maps and struct values.
If you want to test it, pass a map, e.g. bson.M to bson.Marshal():
var x = Base64Data{0x01, 0x02, 0x03}
dd, err := bson.Marshal(bson.M{"data": x})
fmt.Println(string(dd), err)
Your code works as-is, and as you intend it to. Try to insert a wrapper value to verify it:
c := sess.DB("testdb").C("testcoll")
var x = Base64Data{0x01, 0x02, 0x03}
if err := c.Insert(bson.M{
"data": x,
}); err != nil {
panic(err)
}
This will save the data as a string, being the Base64 encoded form.
Of course if you want to load it back into a value of type Base64Data, you will also need to define the SetBSON(raw Raw) error method too (bson.Setter interface).