DHowett / go-plist

A pure Go Apple Property List transcoder
Other
410 stars 96 forks source link

Embedded structs' fields are not omitted #74

Open khpeek opened 1 year ago

khpeek commented 1 year ago

Unlike Go's encoding/json, it would appear that go-plist still marshals the fields of embedded structs even if those structs are nil and the omitempty tag is specified. Consider the following example program:

package main

import (
    "fmt"
    "log"

    "howett.net/plist"
)

type Location struct {
    Name        string
    Type        string
    *Restaurant `plist:",omitempty"`
    *Store      `plist:",omitempty"`
}

type Restaurant struct {
    Menu string
}

type Store struct {
    Merchandise string
}

func main() {
    location := Location{
        Name: "Taqueria Cancun",
        Type: "Restaurant",
        Restaurant: &Restaurant{
            Menu: "Tacos",
        },
    }

    locationPlist, err := plist.MarshalIndent(&location, plist.XMLFormat, "\t")
    if err != nil {
        log.Fatalf("marshal location: %v", err)
    }

    fmt.Println(string(locationPlist))
}

The resulting property list is

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Menu</key>
        <string>Tacos</string>
        <key>Merchandise</key>
        <string/>
        <key>Name</key>
        <string>Taqueria Cancun</string>
        <key>Type</key>
        <string>Restaurant</string>
    </dict>
</plist>

This plist representation contains an empty Merchandise string which is not pertinent to a Restaurant and which I'd like to omit. The encoding/json library does this as seen from the example below:

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Location struct {
    Name string
    Type string
    *Restaurant
    *Store
}

type Restaurant struct {
    Menu string
}

type Store struct {
    Merchandise string
}

func main() {
    location := Location{
        Name: "Taqueria Cancun",
        Type: "Restaurant",
        Restaurant: &Restaurant{
            Menu: "Tacos",
        },
    }

    locationJSON, err := json.Marshal(&location)
    if err != nil {
        log.Fatalf("marshal location: %v", err)
    }

    fmt.Println(string(locationJSON))
}

Running this (and piping the output to jq) results in

{
  "Name": "Taqueria Cancun",
  "Type": "Restaurant",
  "Menu": "Tacos"
}

where the Merchandise key is absent. Should the go-plist library not also have this behavior?

khpeek commented 1 year ago

One way to work around this (which is probably better pattern in general) is to embed the Location struct into the Restaurant and Store structs rather than vice versa:

package main

import (
    "fmt"
    "log"

    "howett.net/plist"
)

type Location struct {
    Name string
    Type string
}

type Restaurant struct {
    Location
    Menu string
}

type Store struct {
    Location
    Merchandise string
}

func main() {
    restaurant := Restaurant{
        Location: Location{
            Name: "Taqueria Cancun",
            Type: "Restaurant",
        },
        Menu: "Tacos",
    }

    locationPlist, err := plist.MarshalIndent(&restaurant, plist.XMLFormat, "\t")
    if err != nil {
        log.Fatalf("marshal location: %v", err)
    }

    fmt.Println(string(locationPlist))
}

This results in the desired output,

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Menu</key>
        <string>Tacos</string>
        <key>Name</key>
        <string>Taqueria Cancun</string>
        <key>Type</key>
        <string>Restaurant</string>
    </dict>
</plist>

However, my point still stands that go-plist's behavior deviates from that of encoding/json in this regard.

DHowett commented 1 year ago

Thanks for the report, and comprehensive investigation! I'm a bit behind on my personal repositories, but I'll try to get to this this week. You're right - this should work like encoding/json. There's a chance that this behavior has changed over time and I missed it, or I never mimicked encoding/json properly. :smile: