Skip to content

Latest commit

 

History

History
264 lines (238 loc) · 6.46 KB

README.md

File metadata and controls

264 lines (238 loc) · 6.46 KB

Documentation Go Report Card Coverage Status MIT License

TagConv

Convert any Go Struct to a Map based on custom struct tags with dot notation.

Background

This package tries to simplify certain use-cases where the same struct needs to be organised/represented differently (eg: mapping data from the db to a presentable API JSON output). This would normally have to be done by having two different structs and manually mapping the data between each other.

This package allows you to use any custom struct tag to define the mapping. This mapping follows the dot-notation convention. Example:

Hello string `mytag:"hello.world"`

The above will result in a map with the JSON equivalent of:

{
    "hello": {
        "world": "hello world"
    }
}

Any number of custom tags can be used to represent the same struct in unlimited number of different ways. For examples, see below.

Usage & Examples

Import the package

import "github.com/dumim/tagconv"

Define your struct with custom struct tags:

type MyStruct struct {
    Age   string `foo:"age"`
    Year  int    `foo:"dob.year"`
    Month int    `foo:"dob.month"`
}

obj := MyStruct{
    Age:   "22",
    Year:  1998,
    Month: 1,
}

tagName = "foo"
myMap, err := ToMap(obj, tagName)
if err != nil {
    panic()
}

This will result in a map that looks like:

myMap = map[string]interface{}{
	"age": "22",
	"dob": map[string]interface{}{
	    "year": 1998,
	    "month": 1,
    }
}

Converting to JSON ...

myMapJSON, err := json.MarshalIndent(myMap, "", "    ")
    if err != nil {
    panic()
}
fmt.Print(myMapJSON)

... will result in something similar to:

{
  "age": "22",
  "dob": {
    "year": 1998,
    "month": 1
  }
}

Multiple struct tags

You can use multiple struct tags for different representation of the same struct: For example, similar to the previous example:

type MyStructMultiple struct {
    Age   string `foo:"age" bar:"details.my_age"`
}

obj := MyStruct{
    Age:   "22",
}

Using tagconv for obj over the foo tag (ToMap(obj, "foo")) will result in:

{
  "age": "22"
}

whereas using bar (ToMap(obj, "bar")) on the same obj will result in:

{
  "details": {
    "my_age": "22"
  }
}

Tag options

  • If a nested struct has a tag, this will create a parent-child relationship with that tag and the tags of the fields within that struct.
  • Dot notation will create a parent-child relationship for every ..
  • Not setting any tag will ignore that field, unless if it's a struct; then it will go inside the struct to check its tags
  • - will explicitly ignore that field. As opposed to above, it will not look inside even if the field is of struct type.
  • ,omitempty can be used similar to the json package to skip fields that have empty values
    • this also handles default empty values ("" for string, 0 for int, etc.). Note that for bool values, false wil be omitted if this option is used since this is the default empty value. To work around this, use *bool (see example)

For an example that includes all the above scenarios see the code below:

More complex example

Given a deeply-nested complex struct with custom tags like below:

type Obj struct {
	Name  string `custom:"name"`
	Text  string `custom:"text"`
	World string `custom:"data.world"` // dot notation inside nested struct
}
type ObjTwo struct {
	Hello string `custom:"hello"`
	Text  string `custom:"data.text"`
}
type ObjThree struct {
	Name  string `custom:"name"`
	Value int    `custom:"value"`
}
type ObjFour struct {
    F1 string `custom:"f1,omitempty"`
    F2 struct {
        F21 string `custom:"f21,omitempty"`
    } `custom:"f2"`
    F3 *string     `custom:"f3, omitempty"` // omitempty with space
    F4 int         `custom:"f4,omitempty"`
    F5 bool        `custom:"f5,omitempty"`
    F6 interface{} `custom:"f6,omitempty"`
    F7 struct {
        F71 string `custom:"f71"`
    } `custom:"f7,omitempty"`
    F8 *bool `custom:"f8,omitempty"` // use pointer to keep false on omitempty
    F9 struct {
        F91 string `custom:"f91"`
    } `custom:"f9,omitempty"`
}
type Example struct {
	Name     string     `custom:"name"`
	Email    string     `custom:"email"`
	Obj      Obj        `custom:"object"`
	ObjTwo   ObjTwo     // no tag
	ObjThree ObjTwo     `custom:"-"` // explicitly ignored
	Id       int        `custom:"id"`
	Call     int        `custom:"data.call"` // top-level dot notation
	ArrayObj []ObjThree `custom:"list"`
	Omit     ObjFour    `custom:omit`
}

The ToMap function can be used to convert this into a JSON/Map based on the values defined in the given custom tag like so.

f := false
obj := Example{
    Name:  "2",
    Email: "3",
    Obj: Obj{
        Name:  "4",
        Text:  "5",
        World: "6",
    },
    ObjTwo: ObjTwo{
        Hello: "1",
        Text:  "2",
    },
    Id:   01,
    Call: 02,
    ArrayObj: []ObjThree{
        {"hi", 1},
        {"world", 2},
    },
	Omit: ObjFour{
      F5: f,
      F8: &f,
    }
}
obj.Omit.F9.F91 = "123"

// get the map from custom tags
tagName = "custom"
myMap, err := ToMap(obj, tagName)
if err != nil {
    panic()
}

myMapJSON, err := json.MarshalIndent(myMap, "", "    ")
if err != nil {
    panic()
}
fmt.Print(myMapJSON)

This will produce a result similar to:

{
    "name": "2",
    "email": "3",
    "object": {
        "name": "4",
        "text": "5",
        "data": {
        "world": "6"
        }
    },
    "hello": "1",
    "data": {
        "text": "2",
        "call": 2
    },
    "id": 1,
    "list": [
        {
            "name": "hi",
            "value": 1
        },
        {
            "name": "world",
            "value": 2
        }
    ],
    "omit": {
        "f8": false,
        "f9": {
            "f91": "123"
        }
    }
}

Testing

Run the go tests using go test ./.. -v


Acknowledgements

Contributing

Contributions are always welcome!