Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkl/decode_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
31 changes: 31 additions & 0 deletions pkl/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
9 changes: 7 additions & 2 deletions pkl/unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down