Skip to content

Instantly share code, notes, and snippets.

@torbjornvatn
Last active August 14, 2017 07:40
Show Gist options
  • Save torbjornvatn/8a9f5b50a7f90c1d3e8cbd317e92763b to your computer and use it in GitHub Desktop.
Save torbjornvatn/8a9f5b50a7f90c1d3e8cbd317e92763b to your computer and use it in GitHub Desktop.
Parsing JSON with optional and mutual exclusive fields in Golang

Motivation

I have this Go application that uploads files to either a S3 bucket or a SFTP server (and potentially Google Cloud Storage buckets in the future) and the location of these are stored in JSON config files. I wanted to

package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/fatih/structs"
)
// An JSON array containing two legal and one illegal
// configuration objects
const inputJson = `
[{
"name": "s3Only",
"s3Destination": {
"s3Bucket": "s3://some-s3-bucket"
}
},
{
"name": "sftpOnly",
"sftpDestination": {
"sftpServer": "sftp.unacast.com"
}
},
{
"name": "both",
"s3Destination": {
"s3Bucket": "s3://some-s3-bucket"
},
"sftpDestination": {
"sftpServer": "sftp.unacast.com"
}
}]
`
// Destination is a common interface representing some place
// on the internet where we can upload files
type Destination interface{}
// S3Destination is used to represent S3 buckets
type S3Destination struct {
S3Bucket string `json:"s3Bucket"`
}
// SFTPDestination is used to represent SFTP servers
type SFTPDestination struct {
SFTPServer string `json:"sftpServer"`
}
// DestinationConf is the parent config object also
// containing a name
type DestinationConf struct {
Name string `json:"name"`
Dest Destination `json:"-"`
}
// String returns a string representation of a DestinationConf
func (dc DestinationConf) String() string {
name := dc.Name
msg := "I'm a config called %v with this destination: %v"
switch dest := dc.Dest.(type) {
case *S3Destination:
return fmt.Sprintf(msg, name, dest.S3Bucket)
case *SFTPDestination:
return fmt.Sprintf(msg, name, dest.SFTPServer)
default:
return fmt.Sprintf("Unknown destination: %+v", dc)
}
}
// AddDestintaion takes a var args with destinations and if there's only one it will
// set it on the DestinationConf, else it will return an error
func (dc *DestinationConf) AddDestination(possibleDestinations ...Destination) error {
for _, d := range possibleDestinations {
// Here we use the eminent github.com/fatih/structs library to check whether the
// destination struct is "empty" or contains value.
// "Empty" structs will be discarded.
if !structs.IsZero(d) {
if dc.Dest != nil {
return errors.New("Can't have more than one destionation per config")
}
dc.Dest = d
}
}
return nil
}
func main() {
// We have to start with parsing the individual configs as raw json messages
var rawConfigs []json.RawMessage
if err := json.Unmarshal([]byte(inputJson), &rawConfigs); err == nil {
for _, rawConfig := range rawConfigs {
// We set up pointers representing the different parts of the parsed config
conf, s3, sftp := &DestinationConf{}, &S3Destination{}, &SFTPDestination{}
err := json.Unmarshal(rawConfig, &struct {
*DestinationConf
*S3Destination `json:"s3Destination"`
*SFTPDestination `json:"sftpDestination"`
}{ // sending the pointers in to Unmarshal to get the values out
// s3 and sftp will only have values in them if they're present
// in the config json
conf, s3, sftp,
})
if err == nil {
// Adding the non empty destination value, failing if both have
// value
if err = conf.AddDestination(s3, sftp); err == nil {
fmt.Printf("%v\n and I'm created from this json:\n%v\n\n", conf, string(rawConfig))
} else {
fmt.Println(err)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment