Convert any Go Struct to a Map based on custom struct tags with dot notation.
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.
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
}
}
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"
}
}
- 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 thejson
package to skip fields that have empty values- this also handles default empty values (
""
forstring
,0
forint
, etc.). Note that forbool
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)
- this also handles default empty values (
For an example that includes all the above scenarios see the code below:
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"
}
}
}
Run the go tests using go test ./.. -v
Contributions are always welcome!