I’m wrapping a REST API that I don’t control to make it more user friendly. The JSON that I can get from it has mostly unintelligible fields, e.g.
type Foo struct {
Bar string `json:"b_Ar"`
Baz string `json:"no_relation_to_baz_whatsoever"`
}
With this I can correctly unmarshal the data coming from the external REST API. However, if someone tries to marshal my structs again, they’ll get the unintelligible fields back that I was trying to hide.
As far as I’m aware, there is no way to specify different field names during marshalling and unmarshalling. Thus, I’m currently doing the following
type Foo struct {
Bar string `json:"bar"`
Baz string `json:"baz"`
}
func (f *Foo) UnmarshalJSON(data []byte) error {
type rawFoo struct {
Bar string `json:"b_Ar"`
Baz string `json:"no_relation_to_baz_whatsoever"`
}
var rf rawFoo
if err := json.Unmarshal(data, &rf); err != nil {
return err
}
f.Bar = rf.Bar
f.Baz = rf.Baz
return nil
}
This works, but is inconvenient to write and maintain due to the boilerplate. I guess I could write a helper function like func(data []byte, f *Foo, rf rawFoo) that uses reflection to figure out the matching internally.
But I wanted to hear from others if there is something simpler that I’m missing. Any ideas on how to improve this?
Unfortunately, I can’t think of an easy/clean way of doing this off the top of my head. I found this issue where somebody wanted to do exactly what you are doing and there was no satisfactory solution:
You could maybe make a new struct tag, then use reflection to create a new type where the json tag is your new struct tag. Something like this:
type Example struct {
Bar string `json:"bar" incomingjson:"b_Ar"`
Baz string `json:"baz" incomingjson:"no_relation_to_baz_whatsoever"`
}
And then to use it, you could do something like this:
func CustomUnmarshal[T any](data []byte) (T, error) {
var returnData T
objType := reflect.TypeOf(returnData)
if objType.Kind() == reflect.Ptr {
objType = objType.Elem()
}
if objType.Kind() != reflect.Struct {
return returnData, fmt.Errorf("expected struct, got %v", objType.Kind())
}
// First thing we are going to do is create a new struct type, replacing json tags with
// values from incomingjson tags.
fields := make([]reflect.StructField, objType.NumField())
for i := 0; i < objType.NumField(); i++ {
f := objType.Field(i)
// If we have an incoming json tag, set json tag on our new struct type
incomingjson := f.Tag.Get("incomingjson")
if len(incomingjson) > 0 {
f.Tag = reflect.StructTag(fmt.Sprintf(`json:"%v"`, incomingjson))
}
fields[i] = f
}
// Then we unmarshal
newEl := reflect.New(reflect.StructOf(fields)).Interface()
err := json.Unmarshal(data, &newEl)
if err != nil {
return returnData, err
}
// Next, set values on our original struct
// This is VERY unsafe at the moment.
sourceRef := reflect.Indirect(reflect.ValueOf(newEl))
destRef := reflect.ValueOf(&returnData).Elem()
for _, field := range fields {
if destRef.FieldByName(field.Name).CanSet() {
destRef.FieldByName(field.Name).Set(sourceRef.FieldByName(field.Name))
}
}
return returnData, err
}
This doesn’t handle nested structs with custom tags of course and is probably pretty unsafe. But here’s a running example:
Maybe that can at least point you in one possible direction. But - I think probably just maintaining two structs like you are currently doing is PROBABLY the easiest solution. It’s verbose, but it works. I wonder if you could just code-gen them?
I agree. The reflect solution you shared (thanks a ton for putting that together!) looks harder to maintain than some boilerplate code.
Probably? However, while the amount of types that I do have is large enough for me to find a solution, it is not yet large enough that I’d consider codegen to be an adequate one