# How CommonGrants Supports Custom Fields: Typed Schema Extensions for Grant Opportunities

> Discover how CommonGrants supports custom fields using typed schema extensions. Enable strong typing and TypeScript autocomplete for your grant opportunities with the withCustomFields extension.

- Repository: [U.S. Department of Health & Human Services/simpler-grants-protocol](https://github.com/hhs/simpler-grants-protocol)
- Tags: how-to-guide
- Published: 2026-03-03

---

**CommonGrants supports custom fields through the `withCustomFields` extension, which merges a base Zod schema with a specification object to create strongly-typed `customFields` validation with full TypeScript autocomplete.**

The CommonGrants SDK—part of the HHS Simpler Grants Protocol—enables developers to extend standard grant opportunity schemas with domain-specific data without sacrificing type safety. By implementing **CommonGrants custom fields** through Zod-based schema composition, the system validates extended data structures at runtime while providing compile-time type inference for registered fields.

## Architecture of the Custom Fields System

The custom fields implementation spans multiple layers of the SDK, from type definitions to client integrations.

**Core Files:**

- **[`src/extensions/types.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/types.ts)** — Defines the `CustomFieldSpec` interface that describes a field’s Zod type, description, and optional UI hints.
- **[`src/extensions/with-custom-fields.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/with-custom-fields.ts)** — Implements the `withCustomFields()` function that injects typed `customFields` into base schemas.
- **[`src/extensions/get-custom-field-value.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/get-custom-field-value.ts)** — Provides the `getCustomFieldValue()` helper for safe runtime extraction of field values.
- **[`src/client/opportunities.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/client/opportunities.ts)** — Integrates custom field schemas into the `OpportunitiesClient` for type-safe API consumption.
- **[`website/src/lib/custom-fields/loader.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/website/src/lib/custom-fields/loader.ts)** — Loads the static registry of custom-field definitions from [`content/custom-fields/index.json`](https://github.com/hhs/simpler-grants-protocol/blob/main/content/custom-fields/index.json) for documentation purposes.

**Execution Flow:**

1. Developers define a `Record<string, CustomFieldSpec>` mapping field keys to their type specifications.
2. The `withCustomFields(baseSchema, specs)` function returns an `ExtendedSchema` that includes the `customFields` property.
3. Calling `ExtendedSchema.parse(json)` validates both base fields and registered custom fields.
4. TypeScript infers `customFields.<key>.value` types, while `getCustomFieldValue()` enables dynamic access with null-safety.

At runtime, the SDK validates unregistered custom fields by passing them through unchanged, but only fields declared in the specification receive strong typing and validation.

## The withCustomFields Extension

The `withCustomFields()` function in [`src/extensions/with-custom-fields.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/with-custom-fields.ts) serves as the primary mechanism for extending schemas. It accepts two parameters: a base Zod schema (such as `OpportunityBaseSchema`) and a specification object containing `CustomFieldSpec` definitions.

The function constructs a new Zod schema that adds a `customFields` property to the base structure. Each key in the specification becomes a typed field within `customFields`, complete with the Zod validators defined in the spec. Because the extension builds on Zod, any custom validation—including regex patterns, enums, or complex object shapes—works without additional configuration.

## Defining Custom Field Specifications

The `CustomFieldSpec` interface in [`src/extensions/types.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/types.ts) describes the metadata and validation rules for each custom field. Specifications include the Zod type definition, an optional description for documentation, and flags indicating whether the field is required.

When you pass these specifications to `withCustomFields()`, the SDK generates TypeScript types that reflect the exact structure of your custom fields, enabling autocomplete and compile-time error detection when accessing `customFields.totalBudget.value` or similar paths.

## Runtime Validation and Type Extraction

For scenarios requiring dynamic field access, the `getCustomFieldValue()` helper in [`src/extensions/get-custom-field-value.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/get-custom-field-value.ts) extracts typed values from parsed objects while gracefully handling `null` or `undefined` cases. This function ensures that even when field keys are determined at runtime, you maintain type safety through TypeScript’s type guards.

## Code Examples

### Extending a Schema with Custom Fields

This example from [`examples/custom-fields.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/examples/custom-fields.ts) demonstrates extending the base opportunity schema with budget and applicant type fields:

```typescript
import { z } from "zod";
import {
  withCustomFields,
  getCustomFieldValue,
} from "../src/extensions";

/* Base Opportunity schema (provided by the SDK) */
const OpportunityBaseSchema = z.object({
  id: z.string(),
  title: z.string(),
  // …other core fields
});

/* Custom field specifications */
const customFieldSpecs = {
  // A required numeric field
  totalBudget: {
    type: z.number().positive(),
    description: "Total budget allocated to the grant",
  },
  // An optional enum field
  applicantType: {
    type: z.enum(["Individual", "Organization", "Other"]),
    description: "Who is applying for the grant",
    required: false,
  },
} as const;

/* Create an extended schema */
const OpportunitySchema = withCustomFields(OpportunityBaseSchema, customFieldSpecs);

/* Example API payload */
const apiResponse = {
  id: "op-123",
  title: "Community Health Grant",
  customFields: {
    totalBudget: { value: 50000 },
    applicantType: { value: "Organization" },
  },
};

/* Parse & type‑safe access */
const parsed = OpportunitySchema.parse(apiResponse);
console.log(parsed.customFields.totalBudget.value); // 50000 (number)
console.log(parsed.customFields.applicantType.value); // "Organization"

```

### Accessing Fields Dynamically

When you need to access fields by variable name, use the runtime helper from [`src/extensions/get-custom-field-value.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/get-custom-field-value.ts):

```typescript
import { getCustomFieldValue } from "../src/extensions";

/* Assume `parsed` from the previous example */
const budget = getCustomFieldValue(parsed.customFields, "totalBudget");
const applicant = getCustomFieldValue(parsed.customFields, "applicantType");

console.log(budget);   // 50000
console.log(applicant); // "Organization"

```

### Integrating with the Opportunities Client

The `OpportunitiesClient` in [`src/client/opportunities.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/client/opportunities.ts) accepts a custom schema option for type-safe API responses:

```typescript
import { OpportunitiesClient } from "../src/client";
import { withCustomFields } from "../src/extensions";

const client = new OpportunitiesClient({ apiKey: "YOUR_API_KEY" });

const schema = withCustomFields(OpportunitiesClient.baseOpportunitySchema, {
  federalAgency: { type: z.string() },
  totalBudget:   { type: z.number().positive() },
});

const opp = await client.getOpportunity("op-123", { schema });
console.log(opp.customFields.federalAgency.value);

```

## Summary

- **CommonGrants custom fields** rely on the `withCustomFields()` extension to merge Zod schemas with domain-specific specifications.
- The `CustomFieldSpec` interface in [`src/extensions/types.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/types.ts) defines field types, descriptions, and validation rules.
- TypeScript generates full autocomplete and compile-time safety for registered custom fields while allowing unregistered fields to pass through validation.
- The `getCustomFieldValue()` helper enables type-safe dynamic access to custom fields at runtime.
- The `OpportunitiesClient` accepts extended schemas to ensure API responses validate against your custom field definitions.

## Frequently Asked Questions

### What happens to unregistered custom fields at runtime?

Unregistered custom fields pass through validation unchanged, allowing flexibility for API responses that include fields not defined in your specification. However, only fields declared in the `CustomFieldSpec` receive strong TypeScript typing and Zod validation according to the implementation in [`src/extensions/with-custom-fields.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/with-custom-fields.ts).

### Can I use complex validation rules like regex or enums?

Yes. Because the system builds on Zod, any validation available in Zod—including regex patterns, enum constraints, or custom refinements—works within your `CustomFieldSpec` definitions. Pass these validators directly to the `type` property in your specification object.

### How do I access custom fields dynamically when the key is determined at runtime?

Use the `getCustomFieldValue()` function exported from [`src/extensions/get-custom-field-value.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/get-custom-field-value.ts). This helper extracts values from the `customFields` object while preserving type information and handling null or undefined cases safely.

### Where are the core custom field types defined in the SDK?

The primary type definitions reside in [`src/extensions/types.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/types.ts), which contains the `CustomFieldSpec` interface and internal type transforms. The schema extension logic lives in [`src/extensions/with-custom-fields.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/with-custom-fields.ts), while runtime utilities are split between [`src/extensions/get-custom-field-value.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/extensions/get-custom-field-value.ts) and the client integration in [`src/client/opportunities.ts`](https://github.com/hhs/simpler-grants-protocol/blob/main/src/client/opportunities.ts).