mapstructure icon indicating copy to clipboard operation
mapstructure copied to clipboard

Implement JSON like unmarshaling interface

Open tubocurarin opened this issue 7 years ago • 7 comments

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^^

tubocurarin avatar Apr 18 '18 12:04 tubocurarin

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)

scags9876 avatar Nov 02 '18 16:11 scags9876

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.

andig avatar Jan 14 '19 18:01 andig

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)
...

solarfly73 avatar Jan 15 '20 18:01 solarfly73

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.

mitchellh avatar Nov 26 '20 22:11 mitchellh

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
	}
}

bored-engineer avatar Nov 28 '20 20:11 bored-engineer

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
	}
}

ghostiam avatar Feb 08 '21 19:02 ghostiam

Is anyone still interested in this?

mthjs avatar Jun 17 '22 12:06 mthjs