Skip to content

fix: align OpenAPI schemas with actual handler responses#313

Open
bburda wants to merge 1 commit intomainfrom
fix/openapi-config-metadata-schema
Open

fix: align OpenAPI schemas with actual handler responses#313
bburda wants to merge 1 commit intomainfrom
fix/openapi-config-metadata-schema

Conversation

@bburda
Copy link
Collaborator

@bburda bburda commented Mar 25, 2026

Summary

  • Audit all OpenAPI schema definitions against actual handler JSON responses and fix every mismatch
  • Replace all inline schemas in path_builder.cpp with SchemaBuilder method calls to prevent future divergence
  • Add SchemaConsistencyTest suite (3 tests) as a safety net for schema registry integrity

Issue


Type

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

  • All 1618 unit tests pass (0 failures)
  • New SchemaConsistencyTest suite validates:
    • All $ref targets resolve to registered schemas
    • All *List schemas reference valid item schemas
    • All required fields exist in properties
  • Per-schema unit tests for ConfigurationMetaData, ConfigurationReadValue, OperationDetail

Schema changes

Resource Old Schema New Schema Reason
Config list ConfigurationParam (name,value required) ConfigurationMetaData (id,name,type) Handler returns metadata without value
Config detail/set ConfigurationParam (name,value) ConfigurationReadValue (id,data) Handler returns SOVD ReadValue format
BulkData descriptor content_type, created_at mimetype, creation_date Match actual handler field names
BulkData category list Objects {id,name} Bare strings Handler returns ["rosbags", ...]
Operation detail Flat OperationItem OperationDetail {item: OperationItem} Handler wraps in item key
Script upload Full ScriptMetadata (7 fields) ScriptUploadResponse (id,name) Handler returns minimal response
Trigger PUT request Full Trigger TriggerUpdateRequest (lifetime) Handler only accepts lifetime
Update prepare/execute/auto 200 + UpdateStatus body 202 no body Handler returns 202 with Location header
Health Missing pipeline/linking Added discovery.pipeline, discovery.linking Handler includes these fields
Root overview Missing auth/tls Added optional auth, tls objects Handler conditionally includes them

Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed
Audit all OpenAPI schema definitions against handler JSON responses and fix every mismatch. Generated API clients (progenitor/Rust, openapi-generator/TS/Python) were failing to deserialize responses because schemas declared fields that handlers never returned. Schema fixes: - Configuration list: ConfigurationParam -> ConfigurationMetaData {id,name,type} - Configuration detail/set: ConfigurationParam -> ConfigurationReadValue {id,data} - BulkData descriptor: content_type/created_at -> mimetype/creation_date - BulkData category list: objects -> bare strings - Operation detail: flat OperationItem -> OperationDetail {item: OperationItem} - Script upload: full ScriptMetadata -> ScriptUploadResponse {id,name} - Trigger PUT request: full Trigger -> TriggerUpdateRequest {lifetime} - Update prepare/execute/automated: 200+body -> 202 no body - Health: add discovery.pipeline and discovery.linking - Root: add optional auth and tls objects Also replace all inline schemas in path_builder.cpp with SchemaBuilder method calls to prevent future divergence. Add SchemaConsistencyTest suite (3 tests) validating $ref resolution, list schema integrity, and required field consistency.
Copilot AI review requested due to automatic review settings March 25, 2026 21:13
@bburda bburda self-assigned this Mar 25, 2026
Copy link
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

This PR updates the gateway’s OpenAPI schema definitions to better match actual JSON responses produced by handlers, and refactors PathBuilder to use centralized SchemaBuilder schemas to reduce future drift.

Changes:

  • Added new OpenAPI component schemas (e.g., ConfigurationMetaData, ConfigurationReadValue, OperationDetail, etc.) and updated route registrations to reference them.
  • Refactored openapi/path_builder.cpp to replace inline schema definitions with SchemaBuilder helpers.
  • Added schema registry consistency tests to validate $ref integrity and basic schema self-consistency.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/ros2_medkit_gateway/test/test_schema_builder.cpp Adds unit tests for new schemas and introduces SchemaConsistencyTest checks for registry integrity.
src/ros2_medkit_gateway/test/test_path_builder.cpp Updates cyclic-subscription POST request schema expectations.
src/ros2_medkit_gateway/src/openapi/schema_builder.hpp Declares new schema builder entry points (config metadata/read value, operation detail, bulk-data category list, etc.).
src/ros2_medkit_gateway/src/openapi/schema_builder.cpp Implements the new schemas and updates the component schema registry.
src/ros2_medkit_gateway/src/openapi/path_builder.cpp Switches multiple endpoints to SchemaBuilder schemas instead of inline definitions.
src/ros2_medkit_gateway/src/http/rest_server.cpp Updates OpenAPI route registry schemas/status codes (operations/config/triggers/scripts/updates).
ASSERT_TRUE(result["post"].contains("requestBody"));
auto req_schema = result["post"]["requestBody"]["content"]["application/json"]["schema"];
EXPECT_TRUE(req_schema["properties"].contains("topic"));
EXPECT_TRUE(req_schema["properties"].contains("observed_resource"));
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

This test asserts that the cyclic-subscription POST request schema contains observed_resource, but the actual create endpoint expects a resource field (plus interval and duration). With the current assertion, the test will keep the OpenAPI requestBody schema aligned to the response shape rather than the real request payload.

Suggested change
EXPECT_TRUE(req_schema["properties"].contains("observed_resource"));
EXPECT_TRUE(req_schema["properties"].contains("resource"));
EXPECT_TRUE(req_schema["properties"].contains("interval"));
EXPECT_TRUE(req_schema["properties"].contains("duration"));
Copilot uses AI. Check for mistakes.

nlohmann::json SchemaBuilder::trigger_update_request_schema() {
return {{"type", "object"},
{"properties", {{"lifetime", {{"type", "number"}, {"description", "New lifetime in seconds"}}}}},
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

TriggerUpdateRequest.lifetime is declared as {type: "number"} here, but the update handler requires an integer (body["lifetime"].is_number_integer()), and rejects non-integers. The schema should use type: "integer" (and ideally set minimum: 1 to match the new_lifetime > 0 validation).

Suggested change
{"properties", {{"lifetime", {{"type", "number"}, {"description", "New lifetime in seconds"}}}}},
{"properties",
{{"lifetime",
{{"type", "integer"},
{"minimum", 1},
{"description", "New lifetime in seconds"}}}}},
Copilot uses AI. Check for mistakes.
.description(std::string("Sets a ROS 2 node parameter value for this ") + et.singular + ".")
.request_body("Configuration value", SB::ref("ConfigurationParam"))
.response(200, "Updated configuration", SB::ref("ConfigurationParam"))
.request_body("Configuration value", SB::ref("ConfigurationReadValue"))
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The PUT configuration endpoint is documented with ConfigurationReadValue as the request body, but the handler only requires a data field (and also supports legacy value) and does not require id in the body (it comes from {config_id}). Using ConfigurationReadValue here makes the OpenAPI contract stricter/different than the real API. Consider adding a dedicated request schema for writes (e.g., {data: ...} / oneOf data|value) and referencing that for .request_body(...).

Suggested change
.request_body("Configuration value", SB::ref("ConfigurationReadValue"))
.request_body("Configuration value", SB::ref("ConfigurationWriteValue"))
Copilot uses AI. Check for mistakes.
Comment on lines 141 to 145
put_op["requestBody"]["required"] = true;
put_op["requestBody"]["content"]["application/json"]["schema"] = schema_builder_.from_ros_msg(topic.type);
put_op["responses"]["200"]["description"] = "Value written successfully";
put_op["responses"]["200"]["content"]["application/json"]["schema"] = {
{"type", "object"}, {"properties", {{"status", {{"type", "string"}}}}}};
put_op["responses"]["200"]["content"]["application/json"]["schema"] = SchemaBuilder::generic_object_schema();

Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

For PUT .../data/{item}, the OpenAPI schemas here still don't match the real handler contract: the handler expects a wrapper body containing {type: string, data: <msg-json>} (not the raw ROS message schema), and it responds with an object containing at least id and data (plus x-medkit). Documenting both request and 200-response as from_ros_msg(...) / generic_object_schema() will cause generated clients to send/expect the wrong shapes.

Copilot uses AI. Check for mistakes.
Comment on lines 409 to +416
nlohmann::json get_op;
get_op["tags"] = nlohmann::json::array({"Bulk Data"});
get_op["summary"] = "List bulk data resources for " + entity_path;
get_op["description"] = "Returns available bulk data resources (snapshots, recordings) for this entity.";
get_op["parameters"] = build_query_params_for_collection();
get_op["responses"]["200"]["description"] = "Successful response";
get_op["responses"]["200"]["content"]["application/json"]["schema"] =
SchemaBuilder::items_wrapper({{"type", "object"},
{"properties",
{{"id", {{"type", "string"}}},
{"name", {{"type", "string"}}},
{"status", {{"type", "string"}}},
{"created_at", {{"type", "string"}}},
{"size_bytes", {{"type", "integer"}}}}}});
SchemaBuilder::items_wrapper(SchemaBuilder::bulk_data_descriptor_schema());
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

build_bulk_data_collection() documents GET .../bulk-data as returning a list of BulkDataDescriptor objects, but the actual handler (BulkDataHandlers::handle_list_categories) returns {items: ["rosbags", ...]} (strings). This makes the per-entity /.../docs OpenAPI spec incorrect for the bulk-data collection path. Consider switching this response schema to SchemaBuilder::bulk_data_category_list_schema() (or SB::ref("BulkDataCategoryList") equivalent) and updating the summary/description accordingly.

Copilot uses AI. Check for mistakes.
{"type", "object"},
{"properties", {{"topic", {{"type", "string"}}}, {"interval_ms", {{"type", "integer"}, {"minimum", 1}}}}},
{"required", {"topic"}}};
post_op["requestBody"]["content"]["application/json"]["schema"] = SchemaBuilder::cyclic_subscription_schema();
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The POST .../cyclic-subscriptions requestBody schema is set to cyclic_subscription_schema(), but the create handler requires fields {resource, interval, duration} (and optional protocol) and does not accept/require id, observed_resource, or event_source (those are response fields). Reusing the response schema here makes generated clients send the wrong payload. Introduce a dedicated create-request schema (e.g., cyclic_subscription_create_request_schema) and use it for the POST requestBody, keeping cyclic_subscription_schema() for the 201 response.

Suggested change
post_op["requestBody"]["content"]["application/json"]["schema"] = SchemaBuilder::cyclic_subscription_schema();
post_op["requestBody"]["content"]["application/json"]["schema"] =
SchemaBuilder::cyclic_subscription_create_request_schema();
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

2 participants