GoLang structure doesn't unmarshal properly when using a custom unmarshal for a nested struct
Asked Answered
D

2

4

We need to use a custom unmarshaler for a struct nested in multiple other structs which don't require a custom unmarshaler. We have lots of structs similar to B struct defined below (similar as in nesting A). The code's output is true false 0 (expected true false 2). Any ideas?


Go Playground example here.

package main

import (
    "fmt"
    "encoding/json"
)

type A struct {
    X bool `json:"x"`
    Y bool `json:"y"`
}

type B struct {
    A
    Z int `json:"z"`
}

func (a *A) UnmarshalJSON(bytes []byte) error {
    var aa struct {
        X string `json:"x"`
        Y string `json:"y"`
    }
    json.Unmarshal(bytes, &aa)

    a.X = aa.X == "123"
    a.Y = aa.Y == "abc"
    return nil
}

const myJSON = `{"x": "123", "y": "fff", "z": 2}`

func main() {
    var b B
    json.Unmarshal([]byte(myJSON), &b)
    fmt.Print(b.X," ",b.Y," ",b.Z)
}

EDIT: question was marked as duplicate here but making A an explicit field will make our API cluttered. Also after making A an explicit field the result is false false 2 so it does not help at all.

Disney answered 12/9, 2018 at 11:24 Comment(4)
Why do you have bools in A but strings in the JSON? If you had matching types, then you wouldn't need to write an unmarshaller at all. Perhaps you can have a method for A that returns the boolean value based on the string values that it already has?Alliance
@Alliance this is just a sample, we receive a string from our front-end but we need a struct that we identify by that string on back-end. Unfortunately, matching types is not an option here.Disney
(1) Make B embed another struct C which contains the fields not contained in A (2) Write an UnmarshalJSON() method for B which unmarshals the same JSON into both B.A and B.C. See play.golang.org/p/roF6hKZJ8Bc The advantage of defining another type C with the fields not in A is that you can delegate unmarshalling it to the json package. (assuming that you have contraints preventing you from matching the types in A and the JSON)Alliance
Just added an answer based on the comment since the question is no longer marked as a duplicate.Alliance
A
5

Since B embeds A, A.UnmarshalJSON() is exposed as B.UnmarshalJSON(). Due to that, B implements json.Unmarshaler and as a result json.Unmarshal() calls B.UnmarshalJSON() which only unmarshal's A's fields. That's the reason B.Z does not get set from the JSON.

This is the easiest way I could think of to get it working in accordance with your constraint of not changing the data types in A:

  1. Make B embed another struct C which contains the fields not contained in A.
  2. Write an UnmarshalJSON() method for B which unmarshals the same JSON into both B.A and B.C. The advantage of defining another type C with the fields not in A is that you can delegate unmarshalling it to the json package.

With the new B.UnmarshalJSON() method, you now have full control to unmarshal the fields outside of A as well.

type A struct {
    X bool `json:"x"`
    Y bool `json:"y"`
}

func (a *A) UnmarshalJSON(bytes []byte) error {
    // the special unmarshalling logic here
}

type C struct {
    Z int `json:"z"`
}

type B struct {
    A
    C
}

func (b *B) UnmarshalJSON(bytes []byte) error {
    if err := json.Unmarshal(bytes, &b.A); err != nil {
        return err
    }
    if err := json.Unmarshal(bytes, &b.C); err != nil {
        return err
    }
    return nil
}
Alliance answered 13/9, 2018 at 1:33 Comment(0)
C
0

If you don't want to have an additional struct you can implement UnmarshalJSON in this way:

func (b *B) UnmarshalJSON(bytes []byte) error {
    if err := json.Unmarshal(bytes, &b.A); err != nil {
        return err
    }
    // auxiliary struct
    aux := &struct {
        Z int `json:"z"`
    }{}
    if err := json.Unmarshal(data, aux); err != nil {
        return err
    }
    b.Z = aux.Z
    return nil
}

With this method you don't have to create another struct, this can be useful if you are already using B in your program. The downside of this method is that you now have to update the data type and json name in two places (struct definition and inside UnmarshalJSON)

Croteau answered 27/7 at 11:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.