Custom fields overview
What are custom fields?
Section titled “What are custom fields?”The CommonGrants protocol defines a core set of fields that are widely applicable across federal, state, local, and private grant opportunities. However, many implementations need fields that are relevant to their specific context but aren’t universal enough to belong in the core protocol.
Custom fields solve this problem by providing a structured way to extend CommonGrants models with additional data. For the formal rules governing how custom fields work (e.g., which models support them, what implementations MUST and SHALL NOT do), see the custom fields section of the specification.
Each custom field includes:
| Property | Description |
|---|---|
name | Name of the custom field |
fieldType | The JSON schema type used when de-serializing the value |
schema | (Optional) Link to the full JSON schema for the field |
value | The field’s value |
description | (Optional) Description of the custom field’s purpose |
Custom fields live in the customFields property of any CommonGrants model, allowing implementations to carry additional data while staying compatible with the base protocol.
About the custom field catalog
Section titled “About the custom field catalog”The custom field catalog is a shared repository of pre-defined custom fields that are not part of the core CommonGrants library but are reused across implementations.
The catalog has two primary goals:
-
Standardize semantically equivalent fields. Without a shared catalog, two different teams might define fields like “agency” with slightly different names, types, or structures. This makes interoperability harder and increases integration costs. The catalog gives implementers a single place to find and reuse existing field definitions before creating their own.
-
Create a path for promoting fields into the core protocol. As custom fields gain adoption across implementations, they become candidates for inclusion in the base protocol. The catalog provides visibility into which fields are widely used, creating a foundation for a “field promotion” pipeline where popular custom fields can move from the catalog into
@common-grants/core.
Using custom fields
Section titled “Using custom fields”Both the TypeScript and Python SDKs provide helper functions for extending base models with typed custom fields.
Defining custom field specs
Section titled “Defining custom field specs”Before adding custom fields to a model, you define a spec for each field that describes its type and (optionally) a schema for validating its value.
import { z } from "zod";import { OpportunityBaseSchema } from "@common-grants/sdk/schemas";import { CustomFieldType } from "@common-grants/sdk/constants";import { withCustomFields } from "@common-grants/sdk/extensions";
// Define a value schema for structured typesconst AgencyValueSchema = z.object({ code: z.string().optional(), name: z.string().optional(),});
// Extend the base schema with typed custom fieldsconst OpportunitySchema = withCustomFields(OpportunityBaseSchema, { agency: { fieldType: CustomFieldType.object, value: AgencyValueSchema, description: "The agency offering this opportunity", }, category: { fieldType: CustomFieldType.string, description: "Grant category", },} as const);from pydantic import BaseModelfrom common_grants_sdk.schemas.pydantic import ( OpportunityBase, CustomFieldType,)from common_grants_sdk.extensions.specs import CustomFieldSpec
# Define a value type for structured typesclass AgencyValue(BaseModel): code: str | None = None name: str | None = None
# Extend the base model with typed custom fieldsOpportunity = OpportunityBase.with_custom_fields( custom_fields={ "agency": CustomFieldSpec( field_type=CustomFieldType.OBJECT, value=AgencyValue, description="The agency offering this opportunity", ), "category": CustomFieldSpec( field_type=CustomFieldType.STRING, value=str, description="Grant category", ), }, model_name="Opportunity",)Parsing data with custom fields
Section titled “Parsing data with custom fields”Once you’ve defined your extended schema, use it to parse and validate incoming data. Registered custom fields get full type safety, while unregistered fields pass through validation with the base CustomField type.
import { getCustomFieldValue } from "@common-grants/sdk/extensions";
// Parse incoming dataconst opportunity = OpportunitySchema.parse(responseData);
// Access custom fields directly on the parsed objectconst agencyField = opportunity.customFields?.agency;console.log(agencyField?.value.code); // typed as string | undefinedconsole.log(agencyField?.value.name); // typed as string | undefined
// Or use getCustomFieldValue() to extract just the valueconst agency = getCustomFieldValue( opportunity.customFields, "agency", AgencyValueSchema,);console.log(agency?.code); // typed as string | undefined
const category = getCustomFieldValue( opportunity.customFields, "category", z.string(),);console.log(category); // typed as string | undefined# Parse incoming dataopp = Opportunity.model_validate(response_data)
# Access custom fields directly on the parsed objectagency_field = opp.custom_fields.agencyprint(agency_field.value.code) # typed as str | Noneprint(agency_field.value.name) # typed as str | None
# Or use get_custom_field_value() to extract just the valueagency = opp.get_custom_field_value("agency", AgencyValue)print(agency.code) # typed as str | None
category = opp.get_custom_field_value("category", str)print(category) # typed as str | NoneContributing custom fields
Section titled “Contributing custom fields”The custom field catalog is open to contributions. If you have a field that could be useful across multiple CommonGrants implementations, we encourage you to propose it.
When to contribute
Section titled “When to contribute”Consider contributing a custom field when:
- You’ve defined a field that other implementers are likely to need
- An existing field in the catalog doesn’t quite fit your use case (propose a modification)
- You want to help standardize a field that multiple implementations are already defining independently
How to contribute
Section titled “How to contribute”New custom fields are proposed by opening a pull request against the CommonGrants repository. At a high level, the process involves:
-
Define the field in TypeSpec. Create a new
.tspfile inwebsite/src/specs/custom-fields/that extendsCustomFieldwith your field’s metadata.Here’s an example based on the
agencyfield:import "@common-grants/core";import "@typespec/json-schema";import "./index.tsp";using CommonGrants.Fields;using JsonSchema;namespace CustomFields.Agency;@extension("x-tags", #[Tags.organization])@extension("x-author", "CommonGrants")@extension("x-version", "0.1.0")@extension("x-valid-schemas",#[ValidSchemas.OpportunityBase, ValidSchemas.CompetitionBase])model CustomAgency extends CustomField {name: "agency";fieldType: CustomFieldType.object;@example(#{code: "CMS",name: "Centers for Medicare & Medicaid Services",})value: {/** The agency's acronym or code */code?: string;/** The full name of the agency */name?: string;};description: "Information about the agency offering this opportunity";} -
Register the field. Add an import for your new file in
website/src/specs/custom-fields/index.tspand add an entry towebsite/src/content/custom-fields/index.jsonwith a schema reference and timestamps. -
Open a pull request. Submit your PR following the contributing guidelines. Include context about why the field is useful and which implementations could benefit from it.
For detailed instructions on building and testing the website locally (including compiling TypeSpec and previewing your changes), see the website’s DEVELOPMENT.md.
Best practices
Section titled “Best practices”- Check the catalog first. Before defining a new field, browse the custom field catalog to see if a similar field already exists. If one is close but not quite right, consider proposing a modification instead.
- Use descriptive names. Field names should clearly convey their purpose. Prefer specific names like
federalOpportunityNumberover generic ones likerefNumber. - Choose the right type. Use the most specific
CustomFieldTypefor your field’s value. For structured data, useobjectwith well-documented properties. - Tag appropriately. Use the tags defined in
index.tsp(e.g.,identifier,federal,organization) to help users discover your field through search and filtering. - Specify valid schemas. Indicate which base models your field applies to (e.g.,
OpportunityBase,CompetitionBase) using thex-valid-schemasextension. - Include examples. Add
@exampledecorators so that users can see realistic sample values in the catalog and generated documentation.