Skip to content

fix: decode typed class instances into interface{} as Object#232

Open
JeroenSoeters wants to merge 1 commit intoapple:mainfrom
JeroenSoeters:fix/decode-typed-class-to-interface
Open

fix: decode typed class instances into interface{} as Object#232
JeroenSoeters wants to merge 1 commit intoapple:mainfrom
JeroenSoeters:fix/decode-typed-class-to-interface

Conversation

@JeroenSoeters
Copy link
Copy Markdown

Problem

When the decoder encounters a typed class instance (e.g., MyModule#MyConfig) and the Go target type is interface{}, it errors with:

cannot decode Pkl value of type 'MyModule#MyConfig' into Go type 'interface{}'.
Define a custom mapping for this using pkl.RegisterMapping

This happens because decodeObject only routes Dynamic objects and explicit Object-typed targets to decodeObjectGeneric. All other typed classes are routed to decodeTyped, which requires a registered Go struct mapping.

Dynamic objects and typed class instances have the same wire format - properties, entries, and elements. The only difference is the class name. There's no structural reason to reject typed classes when the Go target is interface{}.

Use case

We maintain a plugin architecture where plugin authors define typed PKL configuration classes that extend a base class:

// Defined by plugin author
class AgentConfig extends BaseAgentAuthConfig {
    type = "auth-basic"
    authorizedUsers: Listing<AuthorizedUser>
}

The host application deserializes plugin config as pkl.Object (via a pkl.Object-tagged struct field) because it cannot (or shouldn't) know all plugin types at compile time. The config is passed through as json.RawMessage to the plugin process, which handles its own deserialization.

This breaks when the config contains typed class instances (like AuthorizedUser) nested inside the pkl.Object.Properties map - each property value targets interface{}, and typed classes are rejected.

Registering mappings via pkl.RegisterMapping is not viable here because plugin types are defined externally and are not known to the host at compile time.

Fix

When the target type is interface{} and no Go struct mapping is registered for the Pkl class, fall back to decodeObjectGeneric (producing a *pkl.Object). If a mapping IS registered, the existing typed decoding path is used.

This is consistent with how Dynamic objects are already handled - typed classes are structurally identical on the wire.

When the decoder encounters a typed class instance (e.g.
authBasic.Config#AuthorizedUser) and the Go target type is interface{},
fall back to decoding as a generic *Object instead of erroring with
'cannot decode Pkl value of type X into Go type interface{}'.

This happens when typed class instances appear as values inside
pkl.Object.Properties (which is map[string]any). The decoder already
handles Dynamic objects this way; typed classes with no registered Go
mapping should follow the same path.

If a Go struct mapping IS registered for the class, the existing typed
decoding path is still used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant