Last active
February 6, 2025 02:56
-
-
Save nicolashery/3c9d977e1f4f0cbf6ce153b1ed1aa50a to your computer and use it in GitHub Desktop.
Encoding and decoding JSON sum types in Go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"encoding/json" | |
"fmt" | |
) | |
type Item struct { | |
ID string `json:"id"` | |
Name string `json:"name"` | |
} | |
type ActionType int | |
const ( | |
ActionType_CreateItem ActionType = iota | |
ActionType_UpdateItem | |
ActionType_DeleteItem | |
ActionType_DeleteAllItems | |
) | |
// note: `exhaustive` linter will catch if we miss an entry here | |
var ActionTypeStringMap = map[ActionType]string{ | |
ActionType_CreateItem: "create_item", | |
ActionType_UpdateItem: "update_item", | |
ActionType_DeleteItem: "delete_item", | |
ActionType_DeleteAllItems: "delete_all_items", | |
} | |
// note: `exhaustive` linter can't catch missing entry here | |
var ActionTypeValueMap = map[string]ActionType{ | |
"create_item": ActionType_CreateItem, | |
"update_item": ActionType_UpdateItem, | |
"delete_item": ActionType_DeleteItem, | |
"delete_all_items": ActionType_DeleteAllItems, | |
} | |
func (t ActionType) MarshalJSON() ([]byte, error) { | |
return json.Marshal(ActionTypeStringMap[t]) | |
} | |
func (t *ActionType) UnmarshalJSON(data []byte) error { | |
var s string | |
if err := json.Unmarshal(data, &s); err != nil { | |
return err | |
} | |
if v, ok := ActionTypeValueMap[s]; ok { | |
*t = v | |
return nil | |
} | |
return fmt.Errorf("invalid ActionType: %s", s) | |
} | |
func (t ActionType) String() string { | |
return ActionTypeStringMap[t] | |
} | |
//sumtype:decl | |
type Action interface { | |
// sealed interface to emulate sum type | |
isAction() | |
} | |
type CreateItem struct { | |
Item Item `json:"item"` | |
} | |
func (*CreateItem) isAction() {} | |
type UpdateItem struct { | |
Item Item `json:"item"` | |
} | |
func (*UpdateItem) isAction() {} | |
type DeleteItem struct { | |
ID string `json:"id"` | |
} | |
func (*DeleteItem) isAction() {} | |
type DeleteAllItems struct{} | |
func (*DeleteAllItems) isAction() {} | |
func transformAction(action Action) string { | |
var result string | |
// note: `go-check-sumtype` linter will catch if we miss a case here | |
switch v := action.(type) { | |
case *CreateItem: | |
result = fmt.Sprintf("create_item %s %s", v.Item.ID, v.Item.Name) | |
case *UpdateItem: | |
result = fmt.Sprintf("update_item %s %s", v.Item.ID, v.Item.Name) | |
case *DeleteItem: | |
result = fmt.Sprintf("delete_item %s", v.ID) | |
case *DeleteAllItems: | |
result = "delete_all_items" | |
} | |
return result | |
} | |
type ActionWrapper struct { | |
Action Action | |
} | |
func (w *ActionWrapper) MarshalJSONAdjacentlyTagged() ([]byte, error) { | |
var tagged struct { | |
Type ActionType `json:"type"` | |
Value json.RawMessage `json:"value,omitempty"` | |
} | |
// note: `go-check-sumtype` linter will catch if we miss a case here | |
switch w.Action.(type) { | |
case *CreateItem: | |
tagged.Type = ActionType_CreateItem | |
case *UpdateItem: | |
tagged.Type = ActionType_UpdateItem | |
case *DeleteItem: | |
tagged.Type = ActionType_DeleteItem | |
case *DeleteAllItems: | |
tagged.Type = ActionType_DeleteAllItems | |
} | |
value, err := json.Marshal(w.Action) | |
if err != nil { | |
return nil, err | |
} | |
// don't output empty structs | |
if string(value) != "{}" { | |
tagged.Value = value | |
} | |
return json.Marshal(&tagged) | |
} | |
func (w *ActionWrapper) UnmarshalJSONAdjacentlyTagged(data []byte) error { | |
var tagged struct { | |
Type ActionType `json:"type"` | |
Value json.RawMessage `json:"value,omitempty"` | |
} | |
if err := json.Unmarshal(data, &tagged); err != nil { | |
return err | |
} | |
var v Action | |
// note: `exhaustive` linter will catch if we miss a case here | |
switch tagged.Type { | |
case ActionType_CreateItem: | |
v = &CreateItem{} | |
case ActionType_UpdateItem: | |
v = &UpdateItem{} | |
case ActionType_DeleteItem: | |
v = &DeleteItem{} | |
case ActionType_DeleteAllItems: | |
v = &DeleteAllItems{} | |
} | |
if v == nil { | |
return fmt.Errorf("unknown action type: %s", tagged.Type) | |
} | |
if tagged.Value == nil { | |
w.Action = v | |
return nil | |
} | |
if err := json.Unmarshal(tagged.Value, v); err != nil { | |
return err | |
} | |
w.Action = v | |
return nil | |
} | |
func (w *ActionWrapper) MarshalJSONInternallyTagged1() ([]byte, error) { | |
var data []byte | |
var err error | |
// note: `go-check-sumtype` linter will catch if we miss a case here | |
switch v := w.Action.(type) { | |
case *CreateItem: | |
tagged := struct { | |
Type ActionType `json:"type"` | |
CreateItem | |
}{ | |
Type: ActionType_CreateItem, | |
CreateItem: *v, | |
} | |
data, err = json.Marshal(&tagged) | |
case *UpdateItem: | |
tagged := struct { | |
Type ActionType `json:"type"` | |
UpdateItem | |
}{ | |
Type: ActionType_UpdateItem, | |
UpdateItem: *v, | |
} | |
data, err = json.Marshal(&tagged) | |
case *DeleteItem: | |
tagged := struct { | |
Type ActionType `json:"type"` | |
DeleteItem | |
}{ | |
Type: ActionType_DeleteItem, | |
DeleteItem: *v, | |
} | |
data, err = json.Marshal(&tagged) | |
case *DeleteAllItems: | |
tagged := struct { | |
Type ActionType `json:"type"` | |
DeleteAllItems | |
}{ | |
Type: ActionType_DeleteAllItems, | |
DeleteAllItems: *v, | |
} | |
data, err = json.Marshal(&tagged) | |
} | |
return data, err | |
} | |
func (w *ActionWrapper) MarshalJSONInternallyTagged2() ([]byte, error) { | |
v := w.Action | |
data, err := json.Marshal(&v) | |
if err != nil { | |
return nil, err | |
} | |
var tagged map[string]any | |
if err := json.Unmarshal(data, &tagged); err != nil { | |
return nil, err | |
} | |
// note: `go-check-sumtype` linter will catch if we miss a case here | |
switch w.Action.(type) { | |
case *CreateItem: | |
tagged["type"] = ActionType_CreateItem | |
case *UpdateItem: | |
tagged["type"] = ActionType_UpdateItem | |
case *DeleteItem: | |
tagged["type"] = ActionType_DeleteItem | |
case *DeleteAllItems: | |
tagged["type"] = ActionType_DeleteAllItems | |
} | |
return json.Marshal(&tagged) | |
} | |
func (w *ActionWrapper) UnmarshalJSONInternallyTagged(data []byte) error { | |
var tag struct { | |
Type ActionType `json:"type"` | |
} | |
if err := json.Unmarshal(data, &tag); err != nil { | |
return err | |
} | |
var v Action | |
// note: `exhaustive` linter will catch if we miss a case here | |
switch tag.Type { | |
case ActionType_CreateItem: | |
v = &CreateItem{} | |
case ActionType_UpdateItem: | |
v = &UpdateItem{} | |
case ActionType_DeleteItem: | |
v = &DeleteItem{} | |
case ActionType_DeleteAllItems: | |
v = &DeleteAllItems{} | |
} | |
if err := json.Unmarshal(data, v); err != nil { | |
return err | |
} | |
w.Action = v | |
return nil | |
} | |
func (o *ActionWrapper) MarshalJSON() ([]byte, error) { | |
return o.MarshalJSONInternallyTagged2() | |
} | |
func (o *ActionWrapper) UnmarshalJSON(data []byte) error { | |
return o.UnmarshalJSONInternallyTagged(data) | |
} | |
type ActionListWrapper struct { | |
Actions []ActionWrapper `json:"actions"` | |
} | |
func (w *ActionListWrapper) SetActions(actions []Action) { | |
var wrapped []ActionWrapper | |
for _, action := range actions { | |
wrapped = append(wrapped, ActionWrapper{Action: action}) | |
} | |
w.Actions = wrapped | |
} | |
func (w *ActionListWrapper) GetActions() []Action { | |
var actions []Action | |
for _, wrapped := range w.Actions { | |
actions = append(actions, wrapped.Action) | |
} | |
return actions | |
} | |
var ExampleActions = []Action{ | |
&CreateItem{Item: Item{ID: "1", Name: "item1"}}, | |
&UpdateItem{Item: Item{ID: "1", Name: "item1 updated"}}, | |
&DeleteItem{ID: "1"}, | |
&DeleteAllItems{}, | |
} | |
func JSONRoundtrip() { | |
w := &ActionListWrapper{} | |
w.SetActions(ExampleActions) | |
data, err := json.MarshalIndent(w, "", " ") | |
if err != nil { | |
panic(err) | |
} | |
fmt.Println(string(data)) | |
w2 := &ActionListWrapper{} | |
if err := json.Unmarshal(data, w2); err != nil { | |
panic(err) | |
} | |
for _, action := range w2.GetActions() { | |
fmt.Printf("%#v\n", action) | |
} | |
} | |
func PrintTransformedActions() { | |
for _, action := range ExampleActions { | |
fmt.Println(transformAction(action)) | |
} | |
} | |
func main() { | |
JSONRoundtrip() | |
PrintTransformedActions() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment