OpenAPI-to-Terraform Provider Generation

Protocols & Interoperability advanced Building with AI ~33 min read

Overview

OpenAPI code generation is usually discussed in terms of SDKs, server stubs, or documentation. Terraform provider generation is a different target. A provider is not just an HTTP client: it must expose resources, data sources, provider configuration, schema attributes, required/computed semantics, CRUD lifecycle operations, and state behavior that Terraform can reason about during plan and apply.

HashiCorp’s OpenAPI provider spec generator demonstrates a two-stage approach. First, a CLI reads an OpenAPI 3.x document plus a generator configuration file and emits a Provider Code Specification. Second, a framework code generator consumes that intermediate specification and produces Terraform Plugin Framework provider code. The intermediate layer is the key design move: it separates API-shape extraction from provider-code emission.

This pattern is useful beyond Terraform. Any time a general API schema must become a domain-specific integration surface, the generator needs a mapping layer that asks, “which API operations form a durable managed object?” For Terraform, that means mapping create/read/update/delete operations to resources, read operations to data sources, OpenAPI schemas to provider attributes, and field origins to required, optional, or computed behavior.

The source documentation is marked as tech preview, so the exact tool should be treated cautiously. The durable topic is the generation architecture: OpenAPI is the transport contract, generator config supplies product intent, an intermediate provider spec normalizes semantics, and framework-specific codegen turns that normalized spec into maintainable provider code.

Core Concepts

Provider Code Specification

The Provider Code Specification is an intermediate JSON representation of provider configuration, resources, data sources, and schemas. It is not an OpenAPI document and not final Go code. It exists so the OpenAPI mapper can focus on semantic extraction while the framework generator focuses on producing idiomatic Terraform Plugin Framework files.

This boundary makes generation more testable. Teams can diff the provider spec output when an API changes, review resource schemas before code is emitted, and potentially feed the same intermediate spec into multiple code generators.

Generator Config as Intent

OpenAPI describes endpoints, parameters, request bodies, and responses, but it usually does not say which endpoints together represent one Terraform resource. The generator config fills that gap. It names the provider, points to provider configuration schema, and maps each resource to create, read, update, and delete operations by path and method.

This is necessary because Terraform resources are lifecycle abstractions, not HTTP endpoints. A single resource may use a POST request body for desired configuration, a GET response for observed state, path parameters for identity, and optional update/delete operations. The generator must know which operations belong together before it can create a coherent schema.

Required, Optional, and Computed Semantics

Terraform distinguishes values supplied by configuration from values learned from the remote system. The HashiCorp design maps fields from a create request body to required or computed-optional attributes depending on OpenAPI required/default metadata. Fields that appear only in response schemas become computed attributes. Data source parameters become required or computed-optional inputs, while response-only fields become computed outputs.

This semantic mapping is where generic API codegen becomes provider codegen. A client SDK can expose every field as a struct member; a Terraform provider must tell the planner which fields users set, which fields the provider fills, and which fields may change after apply.

Key Concept: OpenAPI Needs a Provider-Semantics Layer

OpenAPI can describe HTTP mechanics, but Terraform needs managed-object semantics. The generator config and provider code specification bridge that gap by grouping operations into resources, merging schemas by lifecycle role, and assigning Terraform attribute behavior from field origin.

How It Works

Generate an Intermediate Provider Spec

The OpenAPI generator CLI takes two inputs: an OpenAPI 3.0 or 3.1 specification and a generator configuration file. The output is a provider code specification JSON file. That output can then become input to a Terraform Plugin Framework code generator.

tfplugingen-openapi generate \
  --config ./generator_config.yml \
  --output ./provider_code_spec.json \
  ./openapi_spec.yml

The configuration identifies which operations should be interpreted as provider, resource, or data-source surfaces. Without this file, the generator would know that POST /thing and GET /thing/{id} exist, but not that together they form a Terraform resource named thing.

Map Resource Operations to a Schema

For resources, the HashiCorp design searches the configured create and read operations for schemas. The create request body is the main schema. Create response, read response, and read parameters are merged on top in priority order. If the create request body is missing, the generator skips the resource because it cannot infer desired configuration.

provider:
  name: examplecloud
  schema_ref: "#/components/schemas/examplecloud_provider_schema"

resources:
  thing:
    create:
      path: /thing
      method: POST
    read:
      path: /thing/{id}
      method: GET
    update:
      path: /thing
      method: PUT
    delete:
      path: /thing/{id}
      method: DELETE

Deep merge rules matter. The design compares attributes by name, recursively merges object and array children, and lets the main schema win when two sources define the same attribute with different types. That avoids making response-only metadata accidentally override desired configuration.

Map OpenAPI Types to Provider Attributes

The mapper translates OpenAPI primitive and container types into Terraform Plugin Framework attribute categories. Booleans become boolean attributes, strings become string attributes, objects become nested attributes, arrays become list or set attributes, and maps become map attributes. A custom array format of set can request set semantics where order should not matter.

def attribute_behavior(field_origin, required=False, has_default=False):
    if field_origin == "create_request" and required and not has_default:
        return "required"
    if field_origin == "create_request":
        return "computed_optional"
    if field_origin in ("create_response", "read_response"):
        return "computed"
    if field_origin == "read_parameter" and required:
        return "required"
    return "computed_optional"

The exact implementation is more detailed, but the principle is stable: attribute behavior is not only a property of the OpenAPI type. It depends on where the field appears in the lifecycle.

Practical Applications

Fast Provider Bootstrapping

Teams exposing a large REST API can bootstrap a Terraform provider without writing every schema by hand. The generated provider still needs review, tests, import behavior, custom diff handling, retries, and documentation, but the first pass can cover repetitive CRUD schemas and reduce drift between the API contract and provider code.

API Drift Detection

Because the generated provider specification is machine-readable, CI can regenerate it whenever the OpenAPI document changes and show a semantic diff. A removed response field, changed parameter type, or new required create property becomes visible before it breaks provider users.

Consistent Internal Platform Providers

Internal platforms often expose many service APIs but lack polished Terraform providers. A generation pipeline lets each service publish an OpenAPI document and a small generator config, producing consistent provider surfaces for platform teams while preserving a single source of truth for HTTP contracts.

Implementation Patterns

Separate Extraction from Code Emission

Keep the OpenAPI-to-provider-spec pass separate from the provider-spec-to-Go pass. This makes the first pass deterministic and easy to review. It also lets teams write validation rules against the intermediate spec: every resource must have a read operation, every required provider field must be documented, and every computed ID must be stable across refresh.

Use Explicit Operation Grouping

Do not infer resources only from path names. APIs frequently use non-uniform routes, nested paths, action endpoints, or different response envelopes. A generator config should explicitly group operations into resource lifecycle roles and data-source reads so reviewers can catch bad mappings before code generation.

# CI sketch for provider generation.
validate-openapi openapi_spec.yml
tfplugingen-openapi generate \
  --config generator_config.yml \
  --output provider_code_spec.json \
  openapi_spec.yml
validate-provider-spec provider_code_spec.json
tfplugingen-framework generate \
  --input provider_code_spec.json \
  --output ./internal/provider

Preserve Manual Extension Points

Generated providers usually need handwritten behavior: authentication helpers, retry policies, import-state functions, custom validators, suppress-diff logic, and upgrade migrations. Keep generated files isolated from handwritten files so regeneration does not overwrite human code.

Performance Considerations

Common Pitfalls

Further Reading