fix: Add typed FormElement schemas and document YesNoField requirements#60
fix: Add typed FormElement schemas and document YesNoField requirements#60
Conversation
- Replace untyped 'elements: array of object' with comprehensive FormElement schema covering all 19 field types and their type-specific properties - Add FormElementChoice and YesNoLabelValue reusable schemas - Add FormResponse schema (forms create/update actually return the form object, not an empty response) - Add FormValidationErrorResponse for 422 errors - Update forms-create endpoint: 201 with FormResponse, add 422 response - Update forms-update endpoint: replace EmptySuccessResponse with FormResponse, add 422 response - Document YesNoField positive/negative/neutral_enabled as required properties with warning that omitting them causes 500 instead of 422 - Add minimal YesNoField JSON example to forms-intro.md - Fix typo: negative property description said 'positive choice' instead of 'negative choice'
4461e2a to
3b30bbc
Compare
There was a problem hiding this comment.
Pull request overview
Updates the Fulcrum REST OpenAPI spec and forms documentation to make form creation/update via the API easier to implement correctly in generated/typed clients.
Changes:
- Added new OpenAPI schemas for
FormElement(typed form elements), choice/label helper schemas, and newFormResponse/FormValidationErrorResponsewrappers. - Updated
FormRequestto typeelementsasFormElement[]and added several form-level properties and required constraints. - Corrected forms create/update endpoint responses to return a form object and document 422 validation errors; fixed a YesNoField docs typo.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| reference/rest-api.json | Adds/updates schemas for typed form elements and adjusts forms create/update request/response typing. |
| reference/FORMS/forms-intro.md | Fixes a YesNoField documentation typo. |
Comments suppressed due to low confidence (2)
reference/rest-api.json:4918
- The forms create endpoint previously documented a
400 Bad Requestresponse, but this change removes it entirely. Even if you’re adding422for validation errors,400is still important for malformed JSON / missing headers and matches how other endpoints (e.g. webhooks create) document both400and422. Recommend restoring400alongside201and422rather than replacing it.
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormResponse"
}
}
}
},
"422": {
"description": "Unprocessable Entity — validation errors (e.g. missing required element properties like disabled, hidden, required). Note: some element validation failures (e.g. YesNoField missing positive/negative) may incorrectly return 500 instead of 422.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormValidationErrorResponse"
}
}
}
}
reference/rest-api.json:5040
- The forms update endpoint previously documented a
400 Bad Requestresponse, but this change removes it entirely.422is useful for validation failures, but400is still relevant for malformed JSON / missing required headers and is documented that way for other endpoints; consider adding400back in addition to422.
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormResponse"
}
}
}
},
"422": {
"description": "Unprocessable Entity — validation errors",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FormValidationErrorResponse"
}
}
}
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "description": "Required for YesNoField. Label/value for the positive (Yes) choice. Example: {\"label\": \"Yes\", \"value\": \"yes\"}" | ||
| }, | ||
| "negative": { | ||
| "$ref": "#/components/schemas/YesNoLabelValue", | ||
| "description": "Required for YesNoField. Label/value for the negative (No) choice. Example: {\"label\": \"No\", \"value\": \"no\"}" | ||
| }, | ||
| "neutral_enabled": { | ||
| "type": "boolean", | ||
| "description": "Required for YesNoField. Whether the N/A option is enabled." | ||
| }, | ||
| "neutral": { | ||
| "$ref": "#/components/schemas/YesNoLabelValue", | ||
| "description": "Required for YesNoField when neutral_enabled is true. Label/value for the neutral (N/A) choice." |
There was a problem hiding this comment.
FormElement describes several properties as “Required for YesNoField …” (positive/negative/neutral_enabled/neutral), but the schema does not actually enforce those requirements based on type. In OpenAPI 3.1 you can model this as a discriminated union (oneOf + discriminator on type) or with if/then/else to make the YesNoField-only properties required when type is YesNoField, and similarly constrain other field-specific properties.
| "description": "Required for YesNoField. Label/value for the positive (Yes) choice. Example: {\"label\": \"Yes\", \"value\": \"yes\"}" | |
| }, | |
| "negative": { | |
| "$ref": "#/components/schemas/YesNoLabelValue", | |
| "description": "Required for YesNoField. Label/value for the negative (No) choice. Example: {\"label\": \"No\", \"value\": \"no\"}" | |
| }, | |
| "neutral_enabled": { | |
| "type": "boolean", | |
| "description": "Required for YesNoField. Whether the N/A option is enabled." | |
| }, | |
| "neutral": { | |
| "$ref": "#/components/schemas/YesNoLabelValue", | |
| "description": "Required for YesNoField when neutral_enabled is true. Label/value for the neutral (N/A) choice." | |
| "description": "Applicable to YesNoField. Label/value for the positive (Yes) choice. Example: {\"label\": \"Yes\", \"value\": \"yes\"}" | |
| }, | |
| "negative": { | |
| "$ref": "#/components/schemas/YesNoLabelValue", | |
| "description": "Applicable to YesNoField. Label/value for the negative (No) choice. Example: {\"label\": \"No\", \"value\": \"no\"}" | |
| }, | |
| "neutral_enabled": { | |
| "type": "boolean", | |
| "description": "Applicable to YesNoField. Whether the N/A option is enabled." | |
| }, | |
| "neutral": { | |
| "$ref": "#/components/schemas/YesNoLabelValue", | |
| "description": "Applicable to YesNoField when neutral_enabled is true. Label/value for the neutral (N/A) choice." |
| "description": "Default value for the field. Required for non-Section types.", | ||
| "oneOf": [ | ||
| {"type": "string"}, | ||
| {"type": "null"} | ||
| ] |
There was a problem hiding this comment.
FormElement currently makes default_value optional and allows null, but the existing Forms reference in this repo lists default_value as required for all form elements and as a string. Either update the documentation to match the new nullable/optional behavior, or adjust the schema (and/or per-type variants) so default_value is required where the API requires it.
| "description": "Default value for the field. Required for non-Section types.", | |
| "oneOf": [ | |
| {"type": "string"}, | |
| {"type": "null"} | |
| ] | |
| "type": "string", | |
| "description": "Default value for the field. Required for non-Section types." |
| "type": "string", | ||
| "description": "ISO 8601 creation timestamp" | ||
| }, | ||
| "updated_at": { | ||
| "type": "string", |
There was a problem hiding this comment.
FormResponse doesn’t mark the top-level form property as required (unlike other Single responses in this spec), and created_at/updated_at are missing format: date-time even though other schemas in this file consistently include it. Consider adding required: ["form"] and format: "date-time" to the timestamp fields for consistency and better client generation.
| "type": "string", | |
| "description": "ISO 8601 creation timestamp" | |
| }, | |
| "updated_at": { | |
| "type": "string", | |
| "type": "string", | |
| "format": "date-time", | |
| "description": "ISO 8601 creation timestamp" | |
| }, | |
| "updated_at": { | |
| "type": "string", | |
| "format": "date-time", |
Summary
While building MCP server tooling that creates forms via the Fulcrum API, I discovered several gaps in the OpenAPI spec that made it difficult to build correct API clients.
Changes
OpenAPI schema improvements (
reference/rest-api.json)New schemas added:
FormElement— comprehensive typed schema for form elements covering all 19 field types with their type-specific properties (positive/negative/neutral_enabledfor YesNoField,choices/multiple/allow_otherfor ChoiceField,elementsfor Section/Repeatable,expressionfor CalculatedField, etc.)FormElementChoice— reusable label/value pair for ChoiceField choicesYesNoLabelValue— reusable label/value pair for YesNoField positive/negative/neutralFormResponse— response schema for forms create/update (the API returns the full form object, not an empty response)FormValidationErrorResponse— error response schema for 422 validation errors (nestedform.errorsformat)Existing schema improvements:
FormRequest—elementswas typed asarray of {"type": "object"}(completely untyped). Now references the newFormElementschema. Addedrequiredconstraint onnameandelements. Added additional form-level properties (status_field,geometry_types,auto_assign,projects_enabled,assignment_enabled,title_field_keys,script)Endpoint corrections:
200 EmptySuccessResponseto201 FormResponse(the API actually returns 201 with the created form). Added422response withFormValidationErrorResponseEmptySuccessResponsetoFormResponse. Added422responseDocumentation improvements (
reference/FORMS/forms-intro.md)positive/negative/neutral_enabledcauses a 500 Internal Server Error instead of a 422 validation errornegativeproperty description incorrectly said "positive choice"Context
These issues were discovered while building fulcrum-app-mcp, which uses the Fulcrum API to create forms programmatically. The untyped
elementsarray and missing YesNoField documentation led to multiple bugs that were difficult to diagnose:disabled/hidden/required→ 422 with unhelpful error messagespositive/negative→ 500 Internal Server Error (no error message at all)