Skip to content

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

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.