mapstructure
mapstructure copied to clipboard
Implement JSON like unmarshaling interface
Hey folks ;)
I think it would be nice if mapstructure would support an unmarshaling interface like for the json module, so custom marshalers can be defined on a per-type basis easily:
type MyType struct {
number int
numberTimesTwo int
}
// For example we call the interface function for mapstructure UnmarshalMap
func (t *MyType) UnmarshalMap(value interface{}) error {
// Although not a real-world usecase, just multiply our read number by 2.
intValue, ok := value.(int)
if !ok {
return errors.New("Can't convert value to int")
}
t.number = value
t.numberTimesTwo = value * 2
return nil
}
Specifically I tried to do custom marshaling inside viper, which internally uses mapstructure to unmarshal from its internal map-representation of config-key-values to my config-struct. But there are no possibilities there to "override" marshaling behaviour for my types and I have to write quite some boilerplate to circumvent that^^
I would also like mapstructure to support gopkg.in/guregu/null.v3 fields, which I believe fits in with this ticket. The null fields there all implement json.Marshaller. (https://github.com/guregu/null/blob/v3.4.0/bool.go#L94)
Looking at https://github.com/mitchellh/mapstructure/blob/master/mapstructure_test.go#L530 and the DecodeHook I'm wondering if this is not already implementable by supplying a hook that implements the MapUnmarshaler behaviour? ~~Have not tried this though.~~
Update
I have the feeling this cannot be done through the existing hooks:
type MapUnmarshaler interface {
UnmarshalMap(interface{}) (interface{}, error)
}
decodeHook := func(from reflect.Type, to reflect.Type, v interface{}) (interface{}, error) {
unmarshalerType := reflect.TypeOf((*MapUnmarshaler)(nil)).Elem()
if to.Implements(unmarshalerType) {
// invoke UnmarshalMap by name
in := []reflect.Value{reflect.New(to).Elem(), reflect.ValueOf(v)}
r := to.MethodByName("UnmarshalMap").Func.Call(in)
// get first return parameter and cast reflect.Value
v = r[0].Interface().(interface{})
}
return v, nil
}
This works up to the point where v has the correct type and value. However, DecodeHook is only able to perform input pre-processing.
Furthermore v cannot- even if the hook were able to do more- be returned or assigned to the result value as this would already be of the generic type (the one that that performs the actual decoding) and cannot accept specific implementations.
If you are in a real pinch and are starting out with a map, you can marshal that back to JSON bytes then get it into your known structure. But if you're ingesting JSON, which has a field mapped to arbitrary JSON, you can specify that as a json.RawMessage instead of a map[string]interface{}. This allows custom UnmarshalJSON functions to be triggered.
type StringOrBool bool
func (*sb StringOrBool) UnmarshalJSON(b []byte) error {
...
}
var testMessage struct {
Custom StringOrBool `json:"custom"`
}
raw := json.RawMessage(`{"custom":false}`)
err := json.Unmarshal(raw, &testMessage)
...
I like this suggestion, there is also #204 which overlaps with this by reusing the text unmarshaler. I see a fit for both. If a PR were to open up adding this I would work on merging it, but it hasn't yet!
Note I also merged #183 which should enable this sort of functionality more manually.
For reference, here's an example of a custom unmarshaler interface via the new DecodeHookFuncValue interface. (I'm sure there is a cleaner way to do the structure creation logic than what I did):
type Unmarshaler interface {
CustomUnmarshalMethod(interface{}) error
}
func UnmarshalerHook() mapstructure.DecodeHookFunc {
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
// If the destination implements the unmarshaling interface
u, ok := to.Interface().(Unmarshaler)
if !ok {
return from.Interface(), nil
}
// If it is nil and a pointer, create and assign the target value first
if to.IsNil() && to.Type().Kind() == reflect.Ptr {
to.Set(reflect.New(to.Type().Elem()))
u = to.Interface().(Unmarshaler)
}
// Call the custom unmarshaling method
if err := u.CustomUnmarshalMethod(from.Interface()); err != nil {
return to.Interface(), err
}
return to.Interface(), nil
}
}
Or use stdlib TextUnmarshaler interface
func UnmarshalerHook() mapstructure.DecodeHookFunc {
return func(from reflect.Value, to reflect.Value) (interface{}, error) {
if to.CanAddr() {
to = to.Addr()
}
// If the destination implements the unmarshaling interface
u, ok := to.Interface().(encoding.TextUnmarshaler)
if !ok {
return from.Interface(), nil
}
// If it is nil and a pointer, create and assign the target value first
if to.IsNil() && to.Type().Kind() == reflect.Ptr {
to.Set(reflect.New(to.Type().Elem()))
u = to.Interface().(encoding.TextUnmarshaler)
}
var text []byte
switch v := from.Interface().(type) {
case string:
text = []byte(v)
case []byte:
text = v
default:
return v, nil
}
if err := u.UnmarshalText(text); err != nil {
return to.Interface(), err
}
return to.Interface(), nil
}
}
Is anyone still interested in this?