-1

I’m working on an API that receives and sends JSON, nothing fancy here.

The first concern I had was that I needed to only work with UTC times and dates across the API, so any date/time received needed to be converted to UTC.

In order to make this work on already existing structs, I’ve implemented the MarshalJSON() method, and it works like a charm. You can see a simplified example below to illustrate with the Contact struct

package main

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

type Contact struct {
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
    CreatedAt time.Time `json:"created_at"`
}

func (c *Contact) MarshalJSON() ([]byte, error) {
    type ContactAlias Contact
    return json.Marshal(&struct {
        *ContactAlias
        CreatedAt time.Time `json:"created_at" sql:"created_at"`
    }{
        ContactAlias: (*ContactAlias)(c),
        CreatedAt:    c.CreatedAt.UTC(),
    })
}

func main() {
    contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}
    s, err := json.Marshal(&contact)
    if err != nil {
        println(err.Error())
    }
    fmt.Printf("STEP 1)\n\n%s\n\n\n", s)
}

Then, I needed to add another field to my Contact struct that is Intro

package main

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

type Contact struct {
    FirstName string    `json:"first_name"`
    LastName  string    `json:"last_name"`
    CreatedAt time.Time `json:"created_at"`
}

// STEP-1
func (c *Contact) MarshalJSON() ([]byte, error) {
    type ContactAlias Contact
    return json.Marshal(&struct {
        *ContactAlias
        CreatedAt time.Time `json:"created_at" sql:"created_at"`
    }{
        ContactAlias: (*ContactAlias)(c),
        CreatedAt:    c.CreatedAt.UTC(),
    })
}

// STEP-2
type ContactWithIntro struct {
    Contact
    Intro string `json:"intro"`
}

func main() {
    contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}
    s, err := json.Marshal(&contact)
    if err != nil {
        println(err.Error())
    }
    fmt.Printf("STEP 1)\n\n%s\n\n\n", s)

    intro := "Hello World!"
    contactWithIntro := ContactWithIntro{
        Contact: *contact,
        Intro:   intro,
    }

    fmt.Printf("STEP 2-a)\n\n%+v\n\n\n", contactWithIntro)

    s, err = json.Marshal(&contactWithIntro)
    if err != nil {
        println(err.Error())
    }
    fmt.Printf("STEP 2-b)\n\n%s\n", s)
}

As you can see, I can’t get the intro field in the contactWithInfo JSON string .

After a few tests, I figured out that if I remove the MarshalJSON, then I get the info field in the final JSON but then time is not UTC anymore … I don’t understand what’s happening.

Thank you for your support.

Below is the link to test the snippet if you want to test this out:

https://goplay.tools/snippet/oCBqGT3AR96

Edit 24/01/2023 - undesired workaround

A solution to work with UTC time would be to create a custom type associated to a custom MarshalJSON method but this implies to cast the time value everywhere it's assigned to the field struct. I would prefer avoid this solution.

package main

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

type Contact struct {
    FirstName string      `json:"first_name"`
    LastName  string      `json:"last_name"`
    CreatedAt DateTimeUTC `json:"created_at"`
}

type DateTimeUTC time.Time

func (c DateTimeUTC) MarshalJSON() ([]byte, error) {
    return json.Marshal(time.Time(c).UTC().Format(time.RFC3339))
}

func main() {
    t := DateTimeUTC(time.Now())
    contact := &Contact{FirstName: "John", LastName: "Doe", CreatedAt: t}
    s, err := json.Marshal(&contact)
    if err != nil {
        println(err.Error())
    }
    fmt.Printf("%s\n", s)
}

snippet here: https://goplay.tools/snippet/V6A11T4z_jy

Big_Boulard
  • 799
  • 1
  • 13
  • 28
  • You can get `intro` field when removing `&` from `json.Marshal` . `s, err = json.Marshal(contactWithIntro)` – N.F. Jan 24 '23 at 00:09
  • @N.F. The point is that if I remove the `&` I don't get the time in UTC. because the custom `MarshalJSON` don't get executed. Thanks for `-1`, plus having misread the question, that's rich, I appreciate it ;) (no worries, it happens even to the best, I'm joking :D) – Big_Boulard Jan 24 '23 at 06:37
  • 1
    You [embedded](https://go.dev/doc/effective_go#embedding) the `Contact` type into `ContactWithIntro`, adding the `MarshalJSON` method to its method set, hence it's called when marshaling `ContactWithIntro`. – JimB Jan 24 '23 at 14:18
  • Hi @JimB I completely overlooked the way embedding works, I assumed that there was a kind of mix between the default method used by the compiler and the one I've created for the embedded type. Thanks for pointing that out – Big_Boulard Jan 24 '23 at 14:56
  • So, I'll have to use the workaround mentioned in the **Edit 24/01/2023** section, or define the custom `marshalJSON` on the outer struct, namely `ContactWithIntro` – Big_Boulard Jan 24 '23 at 15:01

1 Answers1

-1

Because you've embedded the Contact directly the compiler seems to be casting the ContactWithIntro up to Contact, which is why it's ignoring the Intro field.

If you change the declaration:

type ContactWithIntro struct {
    Contact Contact
    Intro string `json:"intro"`
}

It will then marshal correctly.

Also, at the beginning, you don't need to store the pointer to Contact and then pass a pointer to the pointer to json.Marshal:

contact := Contact{FirstName: "John", LastName: "Doe", CreatedAt: time.Now()}  // No & here
...
contactWithIntro := ContactWithIntro{
    Contact: contact,   // No * here
    Intro:   intro,
}

This form is actually a bit cleaner (IMHO)

I've amended your playground: https://goplay.tools/snippet/G4CCz99Cjqx

pilotpin
  • 137
  • 5
  • 1
    "the compiler seems to be casting", it's not casting anything, the embedded type's methods are promoted to the method set of the outer struct type. That's the point of ["embedding"](https://go.dev/doc/effective_go#embedding) in [struct types](https://go.dev/ref/spec#Struct_types) – JimB Jan 24 '23 at 14:22
  • The output doesn't provide the desired one. It creates a JSON containing 2 attributes: `Contact` and `intro` instead of a single JSON object containing all the fields. Here is the output `{"Contact":{"first_name":"John","last_name":"Doe","created_at":"2023-01-24T14:42:01.698Z"},"intro":"Hello World!"` – Big_Boulard Jan 24 '23 at 14:44