Embedded Types
Some resource properties contain nested objects rather than simple values. There are two ways to model these in formae: plain classes and SubResources.
Plain Classes vs SubResources
The choice depends on whether you need field-level hints on the nested object's properties.
Plain Classes
Use plain classes for simple nested structures where you don't need @FieldHint annotations:
// Simple key-value structure - no FieldHints needed
open class Tag {
hidden key: String
hidden value: Any
fixed Key: String = key
fixed Value: Any = value
}
The hidden/fixed pattern handles output transformation (e.g., key → Key).
SubResources
Use SubResources when you need @FieldHint on nested properties:
@formae.SubResourceHint
open class RootDirectory extends formae.SubResource {
@formae.FieldHint {}
path: String?
@formae.FieldHint { createOnly = true } // This requires SubResource!
creationInfo: CreationInfo?
}
The technical requirement: formae's schema system only extracts @FieldHint annotations from properties if the containing class is a SubResource. FieldHints on plain class properties are ignored.
When to Use Each
| Scenario | Use |
|---|---|
| Simple key-value pairs (tags, labels) | Plain class |
| No field-level hints needed | Plain class |
Need createOnly on nested fields |
SubResource |
Need updateMethod / indexField on nested collections |
SubResource |
| Complex nested configuration with multiple hint types | SubResource |
Defining a SubResource
SubResources extend formae.SubResource and use the @SubResourceHint annotation:
import "@formae/formae.pkl"
@formae.SubResourceHint
open class HealthCheck extends formae.SubResource {
@formae.FieldHint {}
path: String
@formae.FieldHint {}
intervalSeconds: Int
@formae.FieldHint { createOnly = true }
protocol: String
}
Then reference it in your Resource:
@formae.ResourceHint {
type = "MyPlugin::Service::Server"
identifier = "ServerId"
}
open class Server extends formae.Resource {
@formae.FieldHint {}
name: String
@formae.FieldHint {}
healthCheck: HealthCheck?
}
Nested SubResources
SubResources can contain other SubResources for deeply nested structures:
@formae.SubResourceHint
open class CreationInfo extends formae.SubResource {
@formae.FieldHint {}
ownerUid: String
@formae.FieldHint {}
ownerGid: String
@formae.FieldHint { createOnly = true }
permissions: String
}
@formae.SubResourceHint
open class RootDirectory extends formae.SubResource {
@formae.FieldHint {}
path: String?
@formae.FieldHint { createOnly = true }
creationInfo: CreationInfo?
}
Collections
For collections of nested objects, the approach depends on your needs:
Plain class collection (no field hints):
open class Tag {
hidden key: String
hidden value: Any
fixed Key: String = key
fixed Value: Any = value
}
@formae.ResourceHint { /* ... */ }
open class Instance extends formae.Resource {
@formae.FieldHint {
updateMethod = "EntitySet"
indexField = "Key"
}
tags: Listing<Tag>?
}
SubResource collection (with field hints):
@formae.SubResourceHint
open class PolicyAttachment extends formae.SubResource {
@formae.FieldHint {}
policyArn: String
@formae.FieldHint { createOnly = true }
attachedAt: String?
}
@formae.ResourceHint { /* ... */ }
open class Role extends formae.Resource {
@formae.FieldHint {
updateMethod = "EntitySet"
indexField = "PolicyArn"
}
policies: Listing<PolicyAttachment>?
}
See Collection Semantics for details on updateMethod and indexField.
Lifecycle
SubResources have no independent lifecycle:
- Create: Serialized as nested data within the parent's Properties
- Read: The plugin returns the full nested structure
- Update: Changes to SubResources trigger an update on the parent Resource
- Delete: Deleting the parent removes all nested data
Summary
Start with plain classes for simple nested objects. Only use SubResource when you need @FieldHint annotations on nested properties - that's when the extra ceremony pays off.