CommonGrants Versioning Strategy: TypeSpec-Driven Semantic Versioning Explained

CommonGrants uses a TypeSpec-driven semantic versioning strategy where each protocol version is represented by a constant in the CommonGrants.Versions namespace, and API artifacts declare their version provenance using @Versioning decorators.

The hhs/simpler-grants-protocol repository implements a declarative versioning system that enables the Common Grants API to evolve safely while giving downstream developers precise control over which version they consume. Every model, field, route, and filter in the core library carries explicit metadata about when it was introduced, renamed, or changed.

How Versioning Works in CommonGrants

The protocol treats versioning as a first-class concern within the TypeSpec schema definitions. Rather than maintaining separate files for each API version, CommonGrants uses decorators to annotate symbols in place, allowing the compiler to generate version-specific OpenAPI specifications from a single source of truth.

Version Constants in the Core Library

All supported protocol versions are defined as constants in the CommonGrants.Versions namespace. These constants serve as the canonical reference points for the @Versioning decorators used throughout the codebase.

In lib/core/lib/core/types.tsp (lines 31-38), the version constants are declared:

namespace CommonGrants.Versions {
  const v0_1 = "0.1.0";
  const v0_2 = "0.2.0";
  const v0_3 = "0.3.0";
}

These constants provide the temporal markers that other files reference when declaring the lifecycle of API artifacts.

TypeSpec Versioning Decorators

The CommonGrants library leverages TypeSpec's built-in versioning library to annotate schema changes. The four primary decorators used are:

  • @Versioning.added() – Marks when a symbol first appeared in the protocol
  • @Versioning.renamedFrom() – Records that a symbol was renamed from a previous identifier
  • @Versioning.typeChangedFrom() – Documents when a field's type was modified
  • @Versioning.useDependency() – Allows consumer specifications to target a specific protocol version

Annotating API Changes with @Versioning

Every change to the CommonGrants protocol is documented through decorators attached directly to the TypeSpec models. This approach creates an explicit audit trail that tooling can use to generate migration guides and versioned client libraries.

Adding New Models and Fields

When new functionality is introduced, developers annotate the model or field with @Versioning.added() and the appropriate version constant. For example, in lib/core/lib/core/models/person.tsp (line 8), a model added in v0.1 would be decorated:

@Versioning.added(CommonGrants.Versions.v0_1)
model Person {
  id: string;
  name: string;
}

Similarly, new fields added in later versions carry the same annotation. This ensures that code generators know exactly which version of the schema includes each property.

Renaming Symbols Across Versions

The protocol maintains backward compatibility by explicitly tracking renames. In lib/core/lib/core/models/form.tsp (line 12), a field renamed in v0.3 would use:

model Form {
  @Versioning.renamedFrom(CommonGrants.Versions.v0_3, "oldFieldName")
  newFieldName: string;
}

The @Versioning.renamedFrom decorator preserves the historical name, allowing older clients to map their existing data to the new schema while newer clients use the current identifier.

Changing Types Safely

When a field's type must evolve, the @Versioning.typeChangedFrom decorator documents the transition. As shown in lib/core/lib/core/filters/numeric.tsp (line 15), this marks when a field changed from one type to another:

model NumericFilter {
  @Versioning.typeChangedFrom(CommonGrants.Versions.v0_2, string)
  value: integer;
}

This pattern ensures that type migrations are machine-readable, enabling automated validation of data across protocol versions.

Consuming Specific Protocol Versions

Implementers can lock their specifications to a specific CommonGrants version using the @Versioning.useDependency decorator. This is demonstrated in templates/quickstart/main.tsp (line 9):

@Versioning.useDependency(CommonGrants.Versions.v0_2)

import "@common-grants/core";

// All imported symbols resolve to their v0.2 definitions

By declaring the dependency explicitly, the TypeSpec compiler resolves all imported symbols to their state as of v0.2, even if the core library has since added new fields or models. This provides stable API contracts for downstream consumers while allowing the protocol to evolve.

Governance and Versioning Strategy Modifications

The versioning strategy itself is subject to governance through the RFC (Request for Comments) process. According to website/src/content/docs/governance/rfc/index.mdx (lines 22-25), proposals can modify how the protocol handles versioning, ensuring the strategy adapts to community needs while maintaining stability.

RFCs that affect versioning must consider backward compatibility and provide clear migration paths, consistent with the existing decorator-based approach.

Summary

  • Explicit version constants in lib/core/lib/core/types.tsp provide canonical version markers (v0_1, v0_2, v0_3).
  • TypeSpec decorators (@Versioning.added, @Versioning.renamedFrom, @Versioning.typeChangedFrom) annotate every API change with its version of origin.
  • Consumer control via @Versioning.useDependency allows implementations to target specific protocol versions for stable contracts.
  • Machine-readable evolution enables automated generation of versioned OpenAPI schemas and migration tooling.
  • RFC governance ensures the versioning strategy can adapt through community-driven proposals.

Frequently Asked Questions

How does CommonGrants handle breaking changes?

CommonGrants handles breaking changes through the @Versioning decorator system. When a field is renamed or its type is changed, the old name or type is preserved in the schema metadata using @Versioning.renamedFrom or @Versioning.typeChangedFrom. This allows the TypeSpec compiler to generate separate OpenAPI specifications for each version, ensuring that existing integrations continue to function while new implementations can adopt the updated schema.

What version of CommonGrants should my implementation target?

You should target the latest stable version that supports the features you need, declared using @Versioning.useDependency(CommonGrants.Versions.v0_X) in your main TypeSpec file. According to the quickstart template in templates/quickstart/main.tsp, locking to a specific version (such as v0.2) ensures your generated API contracts remain stable even as the core library evolves. You can upgrade versions deliberately by changing the dependency constant and testing your integration against the new schema.

Where are the version constants defined?

The version constants are defined in lib/core/lib/core/types.tsp within the CommonGrants.Versions namespace. This file contains the canonical version strings (v0_1, v0_2, v0_3) that serve as the reference points for all @Versioning decorators throughout the codebase. Any new protocol version must be added to this namespace before it can be referenced in other files.

Can the versioning strategy itself be modified?

Yes, the versioning strategy can be modified through the RFC process documented in website/src/content/docs/governance/rfc/index.mdx. Changes to how versions are managed, how deprecations are handled, or how the decorator system functions require community review and approval via RFC. This ensures that modifications to the versioning system itself undergo proper scrutiny and maintain backward compatibility where possible.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →