diff --git a/pkl/decode_struct.go b/pkl/decode_struct.go index ebcbd2c..b0e08da 100644 --- a/pkl/decode_struct.go +++ b/pkl/decode_struct.go @@ -55,6 +55,16 @@ func (d *decoder) decodeObject(typ reflect.Type) (*reflect.Value, error) { if moduleUri == "pkl:base" && name == "Dynamic" || typ.AssignableTo(objectType) { return d.decodeObjectGeneric(moduleUri, name) } + // When the target type is an interface (e.g. interface{}) and there is no + // registered Go struct for this Pkl class, fall back to decoding as a + // generic Object. This allows typed class instances to be deserialized + // when they appear as values inside Dynamic objects or other containers + // that map to interface{}. + if typ.Kind() == reflect.Interface { + if _, hasSchema := d.schemas[name]; !hasSchema { + return d.decodeObjectGeneric(moduleUri, name) + } + } return d.decodeTyped(name, typ) } diff --git a/pkl/decoder_test.go b/pkl/decoder_test.go index 3e98c73..0c8dd4e 100644 --- a/pkl/decoder_test.go +++ b/pkl/decoder_test.go @@ -487,6 +487,37 @@ func TestDecoder_Decode(t *testing.T) { err: fmt.Errorf("encountered unknown object code: %#02x", 0x7F), }, }, + "should successfully decode typed class instance into interface{} as Object": { + typ: reflect.TypeOf((*interface{})(nil)).Elem(), + data: func(t *testing.T, enc *msgpack.Encoder) { + // Encode a typed class instance (e.g. authBasic.Config#AuthorizedUser) + // Same structure as Dynamic but with a custom class name and module URI + assert.NoError(t, enc.EncodeArrayLen(4)) + assert.NoError(t, enc.EncodeInt(codeObject)) + assert.NoError(t, enc.EncodeString("authBasic.Config#AuthorizedUser")) + assert.NoError(t, enc.EncodeString("projectpackage://pkg.pkl-lang.org/authBasic/config@1.0.0#/Config.pkl")) + // members array with 2 properties + assert.NoError(t, enc.EncodeArrayLen(2)) + // property: username + assert.NoError(t, enc.EncodeArrayLen(3)) + assert.NoError(t, enc.EncodeInt(codeObjectMemberProperty)) + assert.NoError(t, enc.EncodeString("username")) + assert.NoError(t, enc.EncodeString("alice")) + // property: password + assert.NoError(t, enc.EncodeArrayLen(3)) + assert.NoError(t, enc.EncodeInt(codeObjectMemberProperty)) + assert.NoError(t, enc.EncodeString("password")) + assert.NoError(t, enc.EncodeString("$2a$10$hashedpassword")) + }, + want: Object{ + ModuleUri: "projectpackage://pkg.pkl-lang.org/authBasic/config@1.0.0#/Config.pkl", + Name: "authBasic.Config#AuthorizedUser", + Properties: map[string]any{"username": "alice", "password": "$2a$10$hashedpassword"}, + Entries: map[any]any{}, + Elements: []any{}, + }, + expectedErr: nil, + }, "should return error for struct with unknown code": { typ: reflect.TypeOf(dummyStruct{}), data: func(t *testing.T, enc *msgpack.Encoder) { diff --git a/pkl/unmarshal_test.go b/pkl/unmarshal_test.go index 3bee36d..4cd0af1 100644 --- a/pkl/unmarshal_test.go +++ b/pkl/unmarshal_test.go @@ -455,8 +455,13 @@ func TestUnmarshal_AnyType(t *testing.T) { func TestUnmarshal_UnknownType(t *testing.T) { var res unknowntype.UnknownType err := pkl.Unmarshal(unknownType, &res) - assert.Error(t, err) - assert.Equal(t, "cannot decode Pkl value of type `PcfRenderer` into Go type `interface {}`. Define a custom mapping for this using `pkl.RegisterMapping`", err.Error()) + // When a typed class instance has no registered Go mapping and the target + // type is interface{}, it should be decoded as a pkl.Object (not error). + assert.NoError(t, err) + obj, ok := res.Res.(pkl.Object) + assert.True(t, ok, "expected pkl.Object, got %T", res.Res) + assert.Equal(t, "PcfRenderer", obj.Name) + assert.Equal(t, "pkl:base", obj.ModuleUri) } func TestUnmarshal_ArraysTooLong(t *testing.T) {