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
175 changes: 175 additions & 0 deletions content/docs/iac/concepts/components/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,181 @@ A component resource must register a unique type name with the base constructor.
For a complete end-to-end walkthrough of building a component from scratch, including setup, implementation, and publishing, see the [Build a Component](/docs/iac/using-pulumi/build-a-component/) guide.
{{< /notes >}}

## Component arguments and type requirements

When authoring components that will be consumed across different languages (multi-language components), the arguments class has specific requirements and limitations due to the need for serialization. These constraints ensure that component arguments can be transmitted to the Pulumi engine and reconstructed across language boundaries.

### Serialization requirements

Component arguments must be serializable, meaning you must convert them to a format that the engine can transmit and reconstruct. This is necessary because:

1. The Pulumi engine needs to understand and validate the inputs
1. Multi-language components need to translate arguments between languages
1. The state needs to be stored and retrieved across deployments

### Supported types

The following types are supported in component arguments:

- **Primitive types**: `string`, `number`/`int`, `boolean`.
- **Arrays/lists**: Arrays of any supported type.
- **Objects/maps**: Objects with properties of supported types.
- **Input wrappers**: Language-specific input types that wrap values:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my (limited) experience, all args to a component should be Input<T>, as are all args to custom resources. If you make and args member a plain string, you cannot use the output of another resource as its value and would instead have to create the resource in an apply, which is not good.

Our blanket recommendations should be:

  1. Wrap every scalar member of an args class in an Input type, e.g. pulumi.Input<string>
  2. (Need to figure out what to say for vector types - I believe they should be e.g. pulumi.Input<string[]>, but I can't find an example - we should ask Providers or Core what they think)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For vectors, it looks like we kinda dodged the issue on awsx.ec2.Vpc (our VPC component). Availability zones are plain types (string[]), for example.

- TypeScript/JavaScript: `pulumi.Input<T>`
- Python: `pulumi.Input[T]`
- Go: `pulumi.StringInput`, `pulumi.IntInput`, etc.
- .NET: `Input<T>`
- Java: `Output<T>`

### Unsupported types

The following types are not supported in component arguments:

- **Union types**: TypeScript union types like `string | number` are not supported due to limitations in schema inference.
- **Functions/callbacks**: Functions cannot be used in component arguments as they cannot be represented in the schema.
- **Platform-specific types**: Types that exist only in one language and cannot be translated.

### Design recommendations

For better usability and maintainability:

- **Avoid deeply nested types**: While complex generic types can be serialized, deeply nested structures make components harder to use and understand. Keep argument structures simple and flat when possible.

**Example of unsupported TypeScript types:**

```typescript
// ❌ This will NOT work - union types are not supported
export interface MyComponentArgs {
value: string | number; // Union type - unsupported
callback: () => void; // Function - unsupported
}

// ✅ This WILL work - use primitives or Input types
export interface MyComponentArgs {
value: pulumi.Input<string>;
count: pulumi.Input<number>;
}
```

### Constructor requirements by language

Each language has specific requirements for component constructors to ensure proper schema generation:

{{< chooser language "typescript,python,go,csharp,java" >}}

{{% choosable language typescript %}}

**Requirements:**

- The constructor must have an argument named exactly `args`
- The `args` parameter must have a type declaration (e.g., `args: MyComponentArgs`)

```typescript
class MyComponent extends pulumi.ComponentResource {
constructor(name: string, args: MyComponentArgs, opts?: pulumi.ComponentResourceOptions) {
super("pkg:index:MyComponent", name, {}, opts);
}
}
```

{{% /choosable %}}
{{% choosable language python %}}

**Requirements:**

- The `__init__` method must have an argument named exactly `args`
- The `args` parameter must have a type annotation (e.g., `args: MyComponentArgs`)

```python
class MyComponent(pulumi.ComponentResource):
def __init__(self, name: str, args: MyComponentArgs, opts: Optional[pulumi.ResourceOptions] = None):
super().__init__('pkg:index:MyComponent', name, None, opts)
```

{{% /choosable %}}
{{% choosable language go %}}

**Requirements:**

- The constructor function should accept a context, name, args struct, and variadic resource options
- The args should be a struct type

```go
func NewMyComponent(ctx *pulumi.Context, name string, args *MyComponentArgs, opts ...pulumi.ResourceOption) (*MyComponent, error) {
myComponent := &MyComponent{}
err := ctx.RegisterComponentResource("pkg:index:MyComponent", name, myComponent, opts...)
if err != nil {
return nil, err
}
return myComponent, nil
}
```

{{% /choosable %}}
{{% choosable language csharp %}}

**Requirements:**

- The constructor must have exactly 3 arguments:
1. A `string` for the name (or any unspecified first argument)
1. An argument that is assignable from `ResourceArgs` (must extend `ResourceArgs`)
1. An argument that is assignable from `ComponentResourceOptions`

```csharp
public class MyComponent : ComponentResource
{
public MyComponent(string name, MyComponentArgs args, ComponentResourceOptions? opts = null)
: base("pkg:index:MyComponent", name, opts)
{
}
}

public sealed class MyComponentArgs : ResourceArgs
{
[Input("value")]
public Input<string>? Value { get; set; }
}
```

{{% /choosable %}}
{{% choosable language java %}}

**Requirements:**

- The constructor must have exactly one argument that extends `ResourceArgs`
- Other arguments (name, options) are not restricted but typically follow the standard pattern

```java
public class MyComponent extends ComponentResource {
public MyComponent(String name, MyComponentArgs args, ComponentResourceOptions opts) {
super("pkg:index:MyComponent", name, null, opts);
}
}

class MyComponentArgs extends ResourceArgs {
@Import(name = "value")
private Output<String> value;

public Output<String> getValue() {
return this.value;
}
}
```

{{% /choosable %}}

{{< /chooser >}}

### Best practices

When designing component arguments:

1. **Wrap all scalar members in Input types**: Every scalar argument should be wrapped in the language's input type (e.g., `pulumi.Input<string>`). This allows users to pass both plain values and outputs from other resources, avoiding the need to use `apply` for resource composition.
1. **Use basic types**: Stick to primitive types, arrays, and basic objects.
1. **Avoid union types**: Instead of a single value with multiple types, consider multiple, mutually exclusive argument members and validate that only one of them has a value in your component constructor.
1. **Document required vs. optional**: Clearly document which arguments are required and which have defaults.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where, though?

  • Documentation belongs in whatever your language's comment format is if it's just local code.
  • Documentation belongs in the README for an IDP-published component for sure.
  • I don't know whether doc comments (like TSDoc) in the source args class get translated to the generated args in the SDK for a local package.
  • I don't know whether/how Go Provider SDK translated comments in the code into docs in the schema file.

@julienp Do you know?

@julienp do you know?

Copy link
Contributor

@julienp julienp Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Python and TypeScript comments on the args types, the output properties of the component class, as well as a comment on the component class itself all get translated into descriptions in the schema.

class MyCompArgs(TypedDict):
  """doc for the args type itself"""

  someinput: Input[str]
  """doc for someinput"""

class MyComp(ComponentResource):
  """doc for component itself"""
  
  something: Output[str]
  """doc for output something"""

  def __init__(self, args: MyCompArgs, ...): ...

For Go there's an Annotate thing in https://github.com/pulumi/pulumi-go-provider

For Java and Dotnet we have not implemented anything to detect comments.

During codegen these descriptions get translated back into the language's comment format, so the generated SDKs will have these docs.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: if dotnet components become a thing, I feel like attributes would make it super easy to do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have dotnet (and java) components, for example https://github.com/pulumi/pulumi-dotnet/blob/main/integration_tests/provider_component_host/Component.cs

No support for descriptions at the moment though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you probably meant that if they become popular and we put some more time into it :)

1. **Follow language conventions**: Use camelCase for schema properties but follow language-specific naming in implementation (snake_case in Python, PascalCase in .NET).

## Creating Child Resources

Component resources often contain child resources. The names of child resources are often derived from the component resources's name to ensure uniqueness. For example, you might use the component resource's name as a prefix. Also, when constructing a resource, children must be registered as such. To do this, pass the component resource itself as the `parent` option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ export interface StaticPageArgs {
}
```

Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types.
Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types. Certain types like union types (e.g., `string | number`) and functions are not supported due to schema inference limitations. For details on type requirements and limitations, see [Component arguments and type requirements](/docs/iac/concepts/components/#component-arguments-and-type-requirements).

{{% /choosable %}}

Expand All @@ -675,7 +675,7 @@ class StaticPageArgs(TypedDict):
"""The HTML content for index.html."""
```

Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types.
Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types. This means certain types like union types and functions are not supported. For details on type requirements and limitations, see [Component arguments and type requirements](/docs/iac/concepts/components/#component-arguments-and-type-requirements).

Python class properties are typically written in lowercase with words separated by underscores, known as [`snake_case`](https://en.wikipedia.org/wiki/Snake_case), however properties in the [Pulumi package schema](https://www.pulumi.com/docs/iac/using-pulumi/extending-pulumi/schema/) are usually written in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case), where capital letters are used to separate words. To follow these conventions, the inferred schema for a component will have translated property names. In our example `index_content` will become `indexContent` in the schema. When using a component, the property names will follow the conventions of that language, for example if we use our component from TypeScript, we would refer to `indexContent`, but if we use it from Python, we would use `index_content`.

Expand All @@ -690,7 +690,7 @@ type StaticPageArgs struct {
}
```

Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types.
Note that argument classes must be *serializable* and use `pulumi.Input` types, rather than the language's default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see [Component arguments and type requirements](/docs/iac/concepts/components/#component-arguments-and-type-requirements).

Go struct fields are typically written in title case, with the first letter capitalized and capital letters used to separate words, however properties in the [Pulumi package schema](https://www.pulumi.com/docs/iac/using-pulumi/extending-pulumi/schema/) are usually written in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case), with the first letter in lowercase and capital letters used to separate words. To follow these conventions, the inferred schema for a component will have translated property names. In our example `IndexContent` will become `indexContent` in the schema. When using a component, the property names will follow the conventions of that language, for example if we use our component from TypeScript, we would refer to `indexContent`, but if we use it from Go, we would use `IndexContent`.

Expand All @@ -707,7 +707,7 @@ public sealed class StaticPageArgs : ResourceArgs {
}
```

Note that argument classes must be *serializable* and use `Pulumi.Input` types, rather than the language's default types.
Note that argument classes must be *serializable* and use `Pulumi.Input` types, rather than the language's default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see [Component arguments and type requirements](/docs/iac/concepts/components/#component-arguments-and-type-requirements).

{{% /choosable %}}

Expand All @@ -733,7 +733,7 @@ class StaticPageArgs extends ResourceArgs {
}
```

Note that argument classes must be *serializable* and use `com.pulumi.core.Output<T>` types, rather than the language's default types.
Note that argument classes must be *serializable* and use `com.pulumi.core.Output<T>` types, rather than the language's default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see [Component arguments and type requirements](/docs/iac/concepts/components/#component-arguments-and-type-requirements).

The `@Import` decorator marks this as a *required* input and allows use to give a name for the input that could be different from the implementation here.

Expand Down
Loading