How to match string golang struct tags

Issue

i have a json data with dynamic key like:

{
  "id_employee": 123
}

or

{
  "id_user": 321
}

I’m trying to unmarshall data to struct.

how can i create a struct tags to match all those 2 key "id_user" and "id_employee" from the example, after unmarshall the data ?

interface User struct {
   Id int64 .....
}

Solution

Before we begin

A small disclaimer: I wrote all the code snippets below off the top of my head, no proof reading, or anything of the sort. The code is not copy-paste ready. The point of this answer is to provide you with some approaches that allow you to do what you’re asking, some explanation as to why it may or may not be a good idea to choose a given option, etc… The third approach is definitely the better approach of the bunch, but given the limited information (no specifics WRT the problem you’re trying to solve), you might need to do some more digging to get to a final solution.

Next, I have to ask why you’re trying to do something like this. If you want a single type you can use to unmarshal different payloads, I think you’re introducing a lot of code smell. If the payloads differ, they must represent different data. Wanting to use a single catch-all type for multiple data-sets IMO is just asking for trouble. I’ll give a couple of ways you can do this, but I want to be very clear on this before I begin:

Even though this is possible, it is a bad idea

A smaller issue, but I have to point it out: you’re including an example type like this:

interface User struct {
    Id int64
}

This is just outright wrong. A struct with fields is not an interface, so I’m going to assume 2 things moving forwards. One is that you want dedicated user types like:

type Employee struct {
    Id int64
}
type Employer struct {
    Id int64
}

And a single:

type User interface {
   ID() int64
}

Unmarshalling this stuff

So there are a number of ways you can accomplish what you’re trying to do. The messy, but simple way is to have a single type that contains all possible permutations of the fields:

type AllUser struct {
    UID int64 `json:"user_id"`
    EID int64 `json:"employee_id"`
}

This ensures that both user_id employee_id fields in your JSON input will find a home, and an ID field will be populated. The real mess quickly becomes apparent when you want to implement the User interface, though:

func (a AllUser) ID() int64 {
    if a.UID != 0 {
        return a.UID
    }
    if a.EID != 0 {
        return a.EID
    }
    // and so on
    return 0 // probably an error?
}

For getters, that’s just a lot of boilerplate to get through, but what about setters? The field may not have been set yet. You’d need to figure out a way to set the correct ID field from a single setter. Passing in an enum/constant to specify what field you’re looking to set may at first seem like a reasonable approach, but think about it: it kind of defeats the purpose of having an interface in the first place. You’d lose any and all abstraction. So this approach is quite flawed.

Furthermore, if you have an employee ID set, the other ID fields will default to their nil values (0 for int64). Marshalling the type again will result in JSON output like this:

{
    "employee_id": 123,
    "user_id": 0,
    "employer_id": 0,
}

You can address this issue by changing your type to use pointers, and add omitempty to skip nil fields from the JSON output:

type AllUser struct {
    EID *int64 `json:"employee_id,omitempty"`
    UID *int64 `json:"user_id,omitempty"`
}

Again, this is nasty business, and will result in you having to deal with pointer fields (which may or may not be nil at different points in time) throughout the code. It’s not that difficult to do, but it adds a lot of noise, makes the code more prone to bugs, and is just all-round a PITA that you should avoid if you can. And you can avoid it quite easily.

Custom marshalling

A better approach would be to create a base type that embeds data-specific types. Assuming we have created our Employee and Employer or Customer types. These types all have an ID field, with their own tags, like this:

type Employee struct {
    ID int64 `json:"employee_id"`
}

type FooUser struct {
    ID int64 `json:"foo_id"`
}

The next thing to do is to create a semi-generic type that embeds all specific user types. Shared fields (e.g. if all data-sets have a name field) can be added on this base type. The next thing you’ll have to do is embed this composite type into yet another type that implements custom marshal/unmarshalling. This will allow you to set some fields (like I’ve included in the example here: a field that specifies that type of user you’re dealing with, for example).

type UserType int

const (
    EmployeeUserType UserType = iota
    FooUserType
    // go-style enum values for all user-types
)

type BaseUser struct {
    WrappedUser
}

type WrappedUser struct {
    *Employee // embed pointers to these types
    *FooUser
    Name       string   `json:"name"`
    Type       UserType `json:"-"` // ignore this in JSON unmarshalling
}

func (b *BaseUser) UnmarshalJSON(data []byte) error {
    if err := json.Unmarshal(data, &b.WrappedUser); err != nil {
        return err
    }
    if b.Employee != nil {
        b.Type = EmployeeUserType // set the user-type flag
    }
    if b.FooUser != nil {
        b.Type = FooUserType
    }
    return nil
}

func (b BaseUser) MarshalJSON() ([]byte, error) {
    return json.Marshal(b.WrappedUser) // wrapped user doesn't have any custom handling
}

To implement the User interface now, you can implement it on the WrappedUser type (the BaseUser embeds it, so the methods will be accessible either way), and you now know precisely what fields you need to get/set because you have the type flag to tell you:

func (w WrappedUser) ID() int64 {
    switch w.Type {
    case EmployeeUserType:
        return w.Employee.ID
    case FooUserType:
        return w.FooUser.ID
    }
    return 0
}

The same can be done with setters:

func (w *WrappedUser) SetID(id int64) {
    switch w.Type {
    case EmployeeUserType:
        if w.Employee == nil {
            w.Employee = &Employee{}
        }
        w.Employee.ID = id
    case FooUserType:
        if w.FooUser == nil {
            w.FooUser = &FooUser{}
        }
        w.FooUser.ID = id
    }
}

Using custom marshalling and embedding types like this is slightly better, but as you probably can tell by looking at this one, pretty simple example already, it quickly becomes quite cumbersome to handle/maintain.

Flipping the script

Now I’m assuming that you want to be able to unmarshal different payloads into a single type because a lot of fields are shared, but things like the ID field might be different (user_id vs employee_id in this case). That’s perfectly normal. You’re asking how you can use a single catch-all type. That’s kind of an X-Y problem. Instead of asking how you can use a single type for all specific data-sets, why not simply create a type for the shared fields, and include that into specific types in turn? It’s very similar to the approach with the custom marshalling, but it’s ~1,000,000 times simpler:

// BaseUser contains all fields all specific user-types share
type BaseUser struct {
    Name   string `json:"name"`
    Active bool   `json:"active"`
    // etc...
}

// Employee is a user, that happens to be an employee
type Employee struct {
    ID int64 `json:"employee_id"`
    BaseUser // embed the other fields that all users share here
}

type FooUser struct {
    ID int64 `json:"foo_id"`
    BaseUser
    Name string `json:"foo_user"` // override the name field of BaseUser
}

Implement all methods for the User interface of on the BaseUser type, and just implement the ID getter/setter on the specific types, and you’re done. If you need to override a field, like I did for Name on the FooUser type, then you just override the getter/setter for that field on that single type:

func (f FooUser) Name() string {
    return f.Name
}
func (f *FooUser) SetName(n string) {
    f.Name = n
}

That’s all you need to do. Nice and easy. You’re consuming JSON data. That implies you’re getting that data from somewhere (either an API, or as a response from a query to some kind of data store). If you’re processing data you requested, you should at the very least know what kind of response data you expect. API’s are contracts: I make call X, and the service responds with either the data I request in a given format, or an error. I query data-set Y from a store, and I either get the requested data, or I don’t get anything (potentially, I get an error).

If you’re ingesting data from a file, or from some service, and you can’t predict what you’re getting back, you need to fix your data-source. You shouldn’t be trying to code around a more fundamental problem. Needs must, I’d spend some time writing a small program that, for instance, reads the source file, unmarshals it into something as crude as a map[string]interface{}, check what keys each object contains, and I’d write the data out into distinct files, grouped by type, so I can ingest the data in a more sane way.

Answered By – Elias Van Ootegem

Answer Checked By – David Marino (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.