I'm currently developing a JSON API for a blog in golang, and I've run into a roadblock trying to handle the serialization and deserialization of blog posts. I want my posts to contain an array of Post Sections that could be a number of things (such as normal paragraphs, images, quotes, etc.). I'm using Mongo for storage (with the amazing mgo library) and I want to save the posts like this:
{
"title": "Blog post",
"sections": [
{
"type": "text",
"content": { "en": "English content", "de": "Deutscher Inhalt" }
},
{
"type": "image",
"content": "https://dummyimage.com/100x100"
},
...more sections
],
...other fields
}
I've tried several solutions to implement this in go and none have really seemed like the "right way" to do it:
- Not caring about the content
This seemed like the obvious solution, just using a simple struct:
type PostSection struct{
Type string
Content interface{}
}
This way, I can pass through whatever the frontend POSTS and save it. However, manipulating the data or validating it becomes impossible, so it's not a good solution.
- Using custom interface serialization
I found this article about serializing interfaces in golang. This seemed great at first, because I could have an interface like this:
type PostSection interface{
Type() string
Content() interface{}
}
and then implement every type like this:
type PostImage string
func (p *PostImage) Type() string {
return "image"
}
func (p *PostImage) Content() interface{} {
return p
}
Optimally, that would've been it, and after implementing MarshalJSON
and UnmarshalJSON
for all my types, it was working fine when using json.Marshal directly on a PostSection object.
However, when serializing or deserializing an entire Post object containing an array of PostSection
s, my custom code was just being ignored and the PostSections would just be treated as the underlying objects (string
or map[string]string
in the examples) when serializing, or result in empty objects when deserializing.
- Writing custom serialization for the entire Post struct
So, the solution I'm currently using but would like to change is custom serialization for the entire Post object. This leads to super ugly code, as I only really need custom code for a single field and so I'm passing through the rest, making the deserialization look similar to this:
p.ID = decoded.ID
p.Author = decoded.Author
p.Title = decoded.Title
p.Intro = decoded.Intro
p.Slug = decoded.Slug
p.TitleImage = decoded.TitleImage
p.Images = decoded.Images
...more fields...
and then, decoding the sections like this:
sections := make([]PostSection, len(decoded.Sections))
for i, s := range decoded.Sections {
if s["type"] == "text" {
content := s["content"].(map[string]interface{})
langs := make(PostText, len(content))
for lang, langContent := range content {
langString := langContent.(string)
langs[lang] = langString
}
sections[i] = &langs
} else if s["type"] == "image" {
content := s["content"].(string)
contentString := PostImage(content)
sections[i] = &contentString
}
}
p.Sections = sections
This is a whole lot of code I'll have to use every time I wanna include PostSections in another form somewhere else (for example in a Newsletter) and it doesn't feel like idiomatic go code by a long shot. Also, there is no error handling for malformed sections - They just cause a panic like this.
Is there a clean solution to this problem?