You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[SDKs] Align how the SDKs handle unregistered custom fields #1121
The TypeScript SDK passes unregistered custom fields through. Pydantic's default drops unknown keys, so the Python SDK silently loses them. extensions/types.py sets extra="allow" for the extension types, but nothing else does, so behavior differs by model as well as by language.
The intended behavior is to preserve unregistered custom fields, so the Python SDK should be updated to match TypeScript. The harder part is making sure unregistered custom fields still conform to the base custom field shape.
Acceptance criteria
Current behavior in both SDKs is documented on this ticket
The Python SDK preserves unregistered custom fields, matching TypeScript
Unregistered custom fields are validated against the base custom field shape in both SDKs
Tests cover unregistered custom fields across both SDKs' supported custom-field extension paths
Public SDK documentation reflects the behavior
The Python and TypeScript changes are included in the [Py SDK] v0.9.0 release and [TS SDK] v0.9.0 release candidates, respectively
Release coordination
This ticket spans both SDKs. GitHub allows only one milestone per issue, so [Py SDK] v0.9.0 release is the primary milestone. Completion also requires the coordinated [TS SDK] v0.9.0 release; do not close this ticket until both package changes are release-ready.
changed the title [-][SDK] Align how the SDKs handle unregistered custom fields[/-][+][SDKs] Align how the SDKs handle unregistered custom fields[/+]on Aug 27, 2026
The TypeScript side of this landed in #1290: the withCustomFields catchall keeps unregistered custom fields and validates them against the base custom field shape. Leaving this open for the matching Python SDK change.
withCustomFields() uses .catchall(CustomFieldSchema), so unregistered entries are kept and validated against the base custom field shape on Opportunity and AwdRecipientIndividual, through definePlugin() and getClient() too.
A missing name, fieldType, or value, an invalid fieldType or schema URL, and a null, string, or array in place of the entry are all rejected. value: null is accepted.
Python (main at 1e1525c4)
Bare OpportunityBase (dict[str, CustomField]) already keeps and validates unregistered entries.
CustomFieldSet containers, which every plugin uses (including the grants.gov plugin), silently drop them, valid or not.
with_custom_fields() keeps them, but only validates object entries that have a fieldType. It fills a missing name from the map key, passes other malformed entries through unchecked, and rewrites the caller's input dict while parsing.
get_custom_field_value() snake_cases the key before looking up an unregistered entry, so vendorPayload comes back as None or as vendor_payload's value.
In both SDKs, unknown keys inside an entry are dropped, and the map key and nested value are otherwise left alone.
#1308 gives both Python containers a typed dict[str, CustomField] extras slot, so unregistered entries are kept under their exact wire key and validated by the same CustomField model the bare base uses. Unregistered entries now need a complete envelope, which is the breaking part for anyone relying on the old with_custom_fields() leniency. It also makes get_custom_field_value() look up unregistered entries by their exact key. TypeScript doesn't change.
Activity
[-][SDK] Align how the SDKs handle unregistered custom fields[/-][+][SDKs] Align how the SDKs handle unregistered custom fields[/+]feat(ts-sdk): add Zod schemas and types for the award models (#1290)
laurabelinfante commentedon Oct 13, 2026
The TypeScript side of this landed in #1290: the
withCustomFieldscatchall keeps unregistered custom fields and validates them against the base custom field shape. Leaving this open for the matching Python SDK change.SnowboardTechie commentedon Oct 14, 2026
Yeah, here's where both SDKs stand before the Python change, for the first AC.
TypeScript (on
mainsince #1290 and #1299)withCustomFields()uses.catchall(CustomFieldSchema), so unregistered entries are kept and validated against the base custom field shape onOpportunityandAwdRecipientIndividual, throughdefinePlugin()andgetClient()too.name,fieldType, orvalue, an invalidfieldTypeorschemaURL, and anull, string, or array in place of the entry are all rejected.value: nullis accepted.Python (
mainat1e1525c4)OpportunityBase(dict[str, CustomField]) already keeps and validates unregistered entries.CustomFieldSetcontainers, which every plugin uses (including the grants.gov plugin), silently drop them, valid or not.with_custom_fields()keeps them, but only validates object entries that have afieldType. It fills a missingnamefrom the map key, passes other malformed entries through unchecked, and rewrites the caller's input dict while parsing.get_custom_field_value()snake_cases the key before looking up an unregistered entry, sovendorPayloadcomes back asNoneor asvendor_payload's value.In both SDKs, unknown keys inside an entry are dropped, and the map key and nested
valueare otherwise left alone.#1308 gives both Python containers a typed
dict[str, CustomField]extras slot, so unregistered entries are kept under their exact wire key and validated by the sameCustomFieldmodel the bare base uses. Unregistered entries now need a complete envelope, which is the breaking part for anyone relying on the oldwith_custom_fields()leniency. It also makesget_custom_field_value()look up unregistered entries by their exact key. TypeScript doesn't change.