2

My data model defines multiple structs that all have two fields in common: a StartDate and an EndDate. I need those two fields to be formatted as 2018-09-21 in the marshalled JSON, therefore the structs implement the Marshaller interface:

type Results struct {
    Source     string    `json:"source"`
    StartDate  time.Time 
    EndDate    time.Time 
}

type WeightedResults struct {
    Source          string           `json:"source"`
    StartDate       time.Time        
    EndDate         time.Time        
}

func (r Results) MarshalJSON() ([]byte, error) {
    type Alias Results
    if equalDate(r.StartDate, r.EndDate) {
        return json.Marshal(&struct {
            Date string `json:"date"`
            Alias
        }{
            Date:  r.StartDate.Format(dateFormat),
            Alias: (Alias)(r),
        })
    }    
    return json.Marshal(&struct {
        StartDate string `json:"start_date"`
        EndDate   string `json:"end_date"`
        Alias
    }{
        StartDate: r.StartDate.Format("2006-01-02"),
        EndDate:   r.EndDate.Format("2006-01-02"),
        Alias:     (Alias)(r),
    })
}

func (r WeightedResults) MarshalJSON() ([]byte, error) {
    type Alias WeightedResults
    if equalDate(r.StartDate, r.EndDate) {
        return json.Marshal(&struct {
            Date string `json:"date"`
            Alias
        }{
            Date:  r.StartDate.Format(dateFormat),
            Alias: (Alias)(r),
        })
    } 
    return json.Marshal(&struct {
        StartDate string `json:"start_date"`
        EndDate   string `json:"end_date"`
        Alias
    }{
        StartDate: r.StartDate.Format("2006-01-02"),
        EndDate:   r.EndDate.Format("2006-01-02"),
        Alias:     (Alias)(r),
    })
}

The solution above works fine but yields lots of code duplication. Is there any way to refactor both implementations of MarshalJSON to use the same logic/code? I am well aware that Go does not offer Generics (yet), but there has to be another way around this issue, right?

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
IggyBlob
  • 372
  • 2
  • 13
  • 1
    If you are able to change the structure of your model/json then you might be able to fix your problem with a "date range" type. (e.g. https://play.golang.org/p/Hmvr0sW9gnj) – mkopriva Sep 21 '18 at 16:00

1 Answers1

6

Your custom marshaler should not be on the structs, but on a custom type that embeds time.Time:

type MyTime struct {
    time.Time
}

func (t MyTime) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.Format("2006-01-02"))
}

Then use this type everywhere you want.

type Results struct {
    Source     string    `json:"source"`
    StartDate  MyTime
    EndDate    MyTime
}
Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
  • Sure, I could do that. However, my custom marshaller implementation is a bit more complex and depends on both `StartDate` and `EndDate`. More specifically, I want to omit `EndDate` if it has the same date as `StartDate` (updated the code for better understanding). – IggyBlob Sep 21 '18 at 14:52
  • 1
    `MarshalJSON()` must return JSON text, yours only returns a "go" string. You have to "wrap" it in quotes, so return: `return []byte(\`"\` + t.Format("2006-01-02") + \`"\`), nil`. Better yet, let the `json` package escape it properly: `return json.Marshal(t.Format("2006-01-02"))`. Also, I'd define the custom marshaler with non-pointer receiver, else it will only work if the field is pointer, or a pointer to the embedder struct is used (method sets). Yet another solution could be to not embed `time.Time`, but create a new type like `type MyTime time.Time`. – icza Sep 21 '18 at 14:54
  • Oh, that is indeed more complex. – Jonathan Hall Sep 21 '18 at 14:55
  • The most straight-forward way would be with a tag: `json:"endTime,omitempty"`, then just don't set the EndTime field if it equals StartTime. But that does depend on "external" logic. But I don't think there's any clean way around that. – Jonathan Hall Sep 21 '18 at 14:57
  • @icza: Thanks, an oversight indeed. – Jonathan Hall Sep 21 '18 at 14:57
  • 1
    @icza: Re `type MyTime time.Time`, I tend to avoid these implementations, since one loses the use of the original type's defined methods. But in some cases, it could still be appropriate. – Jonathan Hall Sep 21 '18 at 14:59
  • 1
    @Flimzy True about losing the methods. Also note that `omitempty` does not work with `time.Time`, for details, see: [Golang JSON omitempty With time.Time Field](https://stackoverflow.com/questions/32643815/golang-json-omitempty-with-time-time-field/32646035#32646035) – icza Sep 21 '18 at 15:01