Skip to content

fix: Add typed FormElement schemas and document YesNoField requirements#60

Open
treyhyde wants to merge 1 commit intov2from
fix/form-element-schemas
Open

fix: Add typed FormElement schemas and document YesNoField requirements#60
treyhyde wants to merge 1 commit intov2from
fix/form-element-schemas

Conversation

@treyhyde
Copy link
Copy Markdown
Member

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_enabled for YesNoField, choices/multiple/allow_other for ChoiceField, elements for Section/Repeatable, expression for CalculatedField, etc.)
  • FormElementChoice — reusable label/value pair for ChoiceField choices
  • YesNoLabelValue — reusable label/value pair for YesNoField positive/negative/neutral
  • FormResponse — 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 (nested form.errors format)

Existing schema improvements:

  • FormRequestelements was typed as array of {"type": "object"} (completely untyped). Now references the new FormElement schema. Added required constraint on name and elements. Added additional form-level properties (status_field, geometry_types, auto_assign, projects_enabled, assignment_enabled, title_field_keys, script)

Endpoint corrections:

  • forms-create — Response changed from 200 EmptySuccessResponse to 201 FormResponse (the API actually returns 201 with the created form). Added 422 response with FormValidationErrorResponse
  • forms-update — Response changed from EmptySuccessResponse to FormResponse. Added 422 response

Documentation improvements (reference/FORMS/forms-intro.md)

  • Added warning callout to YesNoField section: omitting positive/negative/neutral_enabled causes a 500 Internal Server Error instead of a 422 validation error
  • Added minimal YesNoField JSON example showing all required properties
  • Fixed typo: negative property 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 elements array and missing YesNoField documentation led to multiple bugs that were difficult to diagnose:

  1. Fields created without disabled/hidden/required → 422 with unhelpful error messages
  2. YesNoField created without positive/negative → 500 Internal Server Error (no error message at all)
  3. Form create response was documented as empty but actually returns the full form object

@treyhyde treyhyde changed the base branch from main to v2 April 28, 2026 00:30
@treyhyde treyhyde requested review from a team as code owners April 28, 2026 00:30
- 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'
@treyhyde treyhyde force-pushed the fix/form-element-schemas branch from 4461e2a to 3b30bbc Compare April 28, 2026 00:32
Copilot AI review requested due to automatic review settings April 28, 2026 00:32
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new FormResponse / FormValidationErrorResponse wrappers.
  • Updated FormRequest to type elements as FormElement[] 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 Request response, but this change removes it entirely. Even if you’re adding 422 for validation errors, 400 is still important for malformed JSON / missing headers and matches how other endpoints (e.g. webhooks create) document both 400 and 422. Recommend restoring 400 alongside 201 and 422 rather 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 Request response, but this change removes it entirely. 422 is useful for validation failures, but 400 is still relevant for malformed JSON / missing required headers and is documented that way for other endpoints; consider adding 400 back in addition to 422.
        "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.

Comment thread reference/FORMS/forms-intro.md
Comment thread reference/rest-api.json
Comment on lines +2026 to +2038
"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."
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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."

Copilot uses AI. Check for mistakes.
Comment thread reference/rest-api.json
Comment on lines +1983 to +1987
"description": "Default value for the field. Required for non-Section types.",
"oneOf": [
{"type": "string"},
{"type": "null"}
]
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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."

Copilot uses AI. Check for mistakes.
Comment thread reference/rest-api.json
Comment on lines +2187 to +2191
"type": "string",
"description": "ISO 8601 creation timestamp"
},
"updated_at": {
"type": "string",
Copy link

Copilot AI Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"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",

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants