opentofu-schema: Output Type Inference Fix for Module Outputs
The opentofu-schema project builds the schema that editors use to understand OpenTofu configuration. This week it had one small commit, but it touches a real edge case: module outputs that have no declared type. If you write .tofu files and rely on completion and hover, it is worth a short read.
I looked at the last week of activity in opentofu/opentofu-schema. It is a quiet window. There is one commit and one merged pull request, with six lines added and two removed in a single file. This is a narrow bug fix, not a feature week.
What the fix changes
The change is a fix for type inference on outputs with unspecified types, merged as PR #66. It lives in schema/module_schema.go, inside schemaForDependentModuleBlock.
That function builds the schema for a module call. For each output of the called module, it creates a targetable address such as module.name.output_name. Each address needs a type, so editor features can offer attribute completion and validate references.
The old logic was short:
- Start with the type declared on the output.
- If that is the nil type and the output value is not null, use the type of the value.
So when the declared type was nil and the value was null, the type stayed as the nil type. A nil type is not a useful thing to hand to a schema consumer. It means “no type information at all”, which is different from “any type”.
The new fallback order
The new logic starts from a safe default and only narrows it when there is evidence. Roughly:
typ := cty.DynamicPseudoType
if !output.Value.IsNull() {
typ = output.Value.Type()
}
if output.Type != cty.NilType && output.Type != cty.DynamicPseudoType {
typ = output.Type
}
The order of precedence is now:
- A concrete declared type on the output wins.
- Otherwise, the type of a known value is used.
- Otherwise, the type is
cty.DynamicPseudoType.
Two details matter here. First, the default is the dynamic pseudo type, which tells consumers “this can be anything”. Second, a declared type that is itself DynamicPseudoType no longer overrides a more specific inferred value type. Before, a declared nil type was the only case that triggered inference. Now both nil and dynamic declarations are treated as “not specified”.
Why it matters for users
Module outputs in OpenTofu often have no explicit type. Output blocks do not require one, and the real type comes from the expression at apply time. A language server cannot always know it ahead of time.
With a nil type in the schema, a reference to such an output could be treated as untyped data. Depending on how the consumer handles that, you may see missing completions or odd diagnostics when you pass module.foo.some_output into another attribute. Falling back to a dynamic type lets the reference be accepted anywhere, and tightens only when the type is known.
I have not seen a test change in this commit, and the diff only shows the one file. So I cannot say which editor symptom triggered the report. The commit title and the code are the evidence here, and they point at the nil type case.
What to watch
- This is a library level change. You get it when the consumer, such as the OpenTofu language server, updates its dependency on opentofu-schema. Expect a lag before editors show the difference.
- If you saw missing completions on outputs from modules that declare no types, re test after your tooling updates.
- Follow the repository for the next commits. A fix this small often comes with follow ups around other places that infer types from values, such as variables or locals.
- The commit title has a typo, “inferance”. It is only a title, but search for the PR by number if you look for it.