Hi guys,
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 the MarshalJSON is the source of evil, because if I remove it I can get the info
field in the final JSON but then time is not UTC anymore …
The problem is that I don’t understand what’s happening under the hood.
Thank you for your support.
Below is the link to test the snippet:
output
STEP 1)
{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z"}
STEP 2-a)
{Contact:{FirstName:John LastName:Doe CreatedAt:2023-01-23 21:40:24.23 +0100 UTC+1 m=+0.000900097} Intro:Hello World!}
STEP 2-b)
{"first_name":"John","last_name":"Doe","created_at":"2023-01-23T20:40:24.23Z"}
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: Better Go Playground