jsongostructnestedunmarshalling

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


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.


Solution

  • 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
    }