Search code examples
jsongonullsliceunmarshalling

Skip nulls when unmarshalling JSON array into slice of pointers


I have following struct:

type Item struct {
    Id       string     `json:"id"`
    Name     string     `json:"name"`
    Products []*Product `json:"products"`
}

func (i *Item) Transform(input []byte) error {
    return json.Unmarshal(input, i)
}

I have to perform several operations on Products and it's members and it's nested members as well which for instance are []*Variant{} or []*Shipping{} etc.

Because most of slices in Item struct are slice of pointers, my code to process this data looks like this:

for _, product := range i.Products {
    if product == nil {
        continue
    }
    
    for _, variant := range product.Variants {
        if variant == nil {
            continue
        }
        
        for _, shipping := range shippings {
            if shipping == nil {
                continue
            }
      
            // and so on...
        }
    }
}

Is there any way to mimick omitempty on nil values in slice of pointers? Example below.

JSON input:

{
    "products": [
        null,
        {},
        null
    ]
}

output, equivalent to:

input := Item{
    Products: []Product{ {} }, // without nulls
}

I tried to use omitempty on []*Property but it doesn't work. I also tried to use non-pointer values but then Go initialises every null to default struct value.


Solution

  • You could implement a custom json.Unmarshaler.

    type Item struct {
        Id       string      `json:"id"`
        Name     string      `json:"name"`
        Products ProductList `json:"products"`
    }
    
    // Use []*Product if you intend to modify
    // the individual elements in the slice.
    // Use []Product if the elements are read-only.
    type ProductList []*Product
    
    // Implememt the json.Unmarshaler interface.
    // This will cause the encoding/json decoder to
    // invoke the UnmarshalJSON method, instead of
    // performing the default decoding, whenever it
    // encounters a ProductList instance.
    func (ls *ProductList) UnmarshalJSON(data []byte) error {
        // first, do a normal unmarshal
        pp := []*Product{}
        if err := json.Unmarshal(data, &pp); err != nil {
            return err
        }
    
        // next, append only the non-nil values
        for _, p := range pp {
            if p != nil {
                *ls = append(*ls, p)
            }
        }
    
        // done
        return nil
    }
    

    Credit to @blackgreen:

    With Go1.18 and up, you don't have to implement the custom unmarshaling for the other []*Variant{} and []*Shipping{} types. Instead you can use a slice type with a type parameter for the element.

    type SkipNullList[T any] []*T
    
    func (ls *SkipNullList[T]) UnmarshalJSON(data []byte) error {
        pp := []*T{}
        if err := json.Unmarshal(data, &pp); err != nil {
            return err
        }
        for _, p := range pp {
            if p != nil {
                *ls = append(*ls, p)
            }
        }
        return nil
    }
    
    type Item struct {
        Id       string                `json:"id"`
        Name     string                `json:"name"`
        Products SkipNullList[Product] `json:"products"`
    }
    
    type Product struct {
        // ...
        Variants SkipNullList[Variant] `json:"variants"`
    }
    
    type Variant struct {
        // ...
        Shippings SkipNullList[Shipping] `json:"shippings"`
    }
    

    https://go.dev/play/p/az_9Mb_RBKX