Skip to content

clean up output text of validator#1077

Merged
nan-yu merged 4 commits intogoogle:mainfrom
wrenj:vvv3
Apr 7, 2026
Merged

clean up output text of validator#1077
nan-yu merged 4 commits intogoogle:mainfrom
wrenj:vvv3

Conversation

@wrenj
Copy link
Copy Markdown
Collaborator

@wrenj wrenj commented Apr 6, 2026

Running validate on the mcp apps recipe json.

BEFORE:
ValueError: Validation failed: {'version': 'v0.9', 'updateComponents': {'surfaceId': 'recipe-card', 'components': [{'id': 'root', 'component': 'Card', 'child': 'main-column'}, {'id': 'main-column', 'component': 'Column', 'children': ['recipe-image', 'content'], 'gap': 'small'}, {'id': 'recipe-image', 'component': 'Image', 'url': {'path': '/image'}, 'altText': {'path': '/title'}, 'fit': 'cover'}, {'id': 'content', 'component': 'Column', 'children': ['title', 'rating-row', 'times-row', 'servings'], 'gap': 'small'}, {'id': 'title', 'component': 'Text', 'text': {'path': '/title'}, 'usageHint': 'h3'}, {'id': 'rating-row', 'component': 'Row', 'children': ['star-icon', 'rating', 'review-count'], 'gap': 'small', 'alignment': 'center'}, {'id': 'star-icon', 'component': 'Icon', 'name': {'literalString': 'star'}}, {'id': 'rating', 'component': 'Text', 'text': {'path': '/rating'}, 'usageHint': 'body'}, {'id': 'review-count', 'component': 'Text', 'text': {'path': '/reviewCount'}, 'usageHint': 'caption'}, {'id': 'times-row', 'component': 'Row', 'children': ['prep-time', 'cook-time'], 'gap': 'medium'}, {'id': 'prep-time', 'component': 'Row', 'children': ['prep-icon', 'prep-text'], 'gap': 'small', 'alignment': 'center'}, {'id': 'prep-icon', 'component': 'Icon', 'name': {'literalString': 'timer'}}, {'id': 'prep-text', 'component': 'Text', 'text': {'path': '/prepTime'}, 'usageHint': 'caption'}, {'id': 'cook-time', 'component': 'Row', 'children': ['cook-icon', 'cook-text'], 'gap': 'small', 'alignment': 'center'}, {'id': 'cook-icon', 'component': 'Icon', 'name': {'literalString': 'shoppingCart'}}, {'id': 'cook-text', 'component': 'Text', 'text': {'path': '/cookTime'}, 'usageHint': 'caption'}, {'id': 'servings', 'component': 'Text', 'text': {'path': '/servings'}, 'usageHint': 'caption'}]}} is not valid under any of the given schemas
E Context failures:
E - 'createSurface' is a required property
E - Additional properties are not allowed ('updateComponents' was unexpected)
E - {'id': 'main-column', 'component': 'Column', 'children': ['recipe-image', 'content'], 'gap': 'small'} is not valid under any of the given schemas
E - {'id': 'recipe-image', 'component': 'Image', 'url': {'path': '/image'}, 'altText': {'path': '/title'}, 'fit': 'cover'} is not valid under any of the given schemas
E - {'id': 'content', 'component': 'Column', 'children': ['title', 'rating-row', 'times-row', 'servings'], 'gap': 'small'} is not valid under any of the given schemas
E - {'id': 'title', 'component': 'Text', 'text': {'path': '/title'}, 'usageHint': 'h3'} is not valid under any of the given schemas
E - {'id': 'rating-row', 'component': 'Row', 'children': ['star-icon', 'rating', 'review-count'], 'gap': 'small', 'alignment': 'center'} is not valid under any of the given schemas
E - {'id': 'star-icon', 'component': 'Icon', 'name': {'literalString': 'star'}} is not valid under any of the given schemas
E - {'id': 'rating', 'component': 'Text', 'text': {'path': '/rating'}, 'usageHint': 'body'} is not valid under any of the given schemas
E - {'id': 'review-count', 'component': 'Text', 'text': {'path': '/reviewCount'}, 'usageHint': 'caption'} is not valid under any of the given schemas
E - {'id': 'times-row', 'component': 'Row', 'children': ['prep-time', 'cook-time'], 'gap': 'medium'} is not valid under any of the given schemas
E - {'id': 'prep-time', 'component': 'Row', 'children': ['prep-icon', 'prep-text'], 'gap': 'small', 'alignment': 'center'} is not valid under any of the given schemas
E - {'id': 'prep-icon', 'component': 'Icon', 'name': {'literalString': 'timer'}} is not valid under any of the given schemas
E - {'id': 'prep-text', 'component': 'Text', 'text': {'path': '/prepTime'}, 'usageHint': 'caption'} is not valid under any of the given schemas
E - {'id': 'cook-time', 'component': 'Row', 'children': ['cook-icon', 'cook-text'], 'gap': 'small', 'alignment': 'center'} is not valid under any of the given schemas
E - {'id': 'cook-icon', 'component': 'Icon', 'name': {'literalString': 'shoppingCart'}} is not valid under any of the given schemas
E - {'id': 'cook-text', 'component': 'Text', 'text': {'path': '/cookTime'}, 'usageHint': 'caption'} is not valid under any of the given schemas
E - {'id': 'servings', 'component': 'Text', 'text': {'path': '/servings'}, 'usageHint': 'caption'} is not valid under any of the given schemas
E - 'updateDataModel' is a required property
E - Additional properties are not allowed ('updateComponents' was unexpected)

AFTER:
E ValueError: Validation failed:
E - messages[1].updateComponents.components[id='main-column']: 'gap' was unexpected
E - messages[1].updateComponents.components[id='recipe-image'].url: {'path': '/image'} is not of type 'string'
E - messages[1].updateComponents.components[id='recipe-image']: 'altText', 'fit' were unexpected
E - messages[1].updateComponents.components[id='content']: 'gap' was unexpected
E - messages[1].updateComponents.components[id='title']: 'usageHint' was unexpected
E - messages[1].updateComponents.components[id='rating-row']: Unknown component: Row
E - messages[1].updateComponents.components[id='star-icon'].name: {'literalString': 'star'} is not of type 'string'
E - messages[1].updateComponents.components[id='rating']: 'usageHint' was unexpected
E - messages[1].updateComponents.components[id='review-count']: 'usageHint' was unexpected
E - messages[1].updateComponents.components[id='times-row']: Unknown component: Row
E - messages[1].updateComponents.components[id='prep-time']: Unknown component: Row
E - messages[1].updateComponents.components[id='prep-icon'].name: {'literalString': 'timer'} is not of type 'string'
E - messages[1].updateComponents.components[id='prep-text']: 'usageHint' was unexpected
E - messages[1].updateComponents.components[id='cook-time']: Unknown component: Row
E - messages[1].updateComponents.components[id='cook-icon'].name: {'literalString': 'shoppingCart'} is not of type 'string'
E - messages[1].updateComponents.components[id='cook-text']: 'usageHint' was unexpected
E - messages[1].updateComponents.components[id='servings']: 'usageHint' was unexpected

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a custom validation flow for A2UI version 0.9, refactoring the validate method to support version-specific logic and adding detailed error reporting for component and data model updates. The review feedback highlights several areas for improvement: optimizing performance by caching Draft202012Validator instances, refactoring duplicated integrity check logic into a shared helper, ensuring safe access to schema definitions to prevent potential KeyError exceptions, and adopting more robust string parsing for validation error messages.

Comment on lines +425 to +430
temp_schema = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$ref": f"catalog.json#/components/{comp_type}"
}

validator = Draft202012Validator(temp_schema, registry=self._validator._registry)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Creating a new Draft202012Validator instance for every component in a loop is inefficient and will cause significant performance degradation for large payloads. These validators should be cached by comp_type.

    cache = self.__dict__.setdefault("_comp_validator_cache", {})
    if comp_type not in cache:
      temp_schema = {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "$ref": f"catalog.json#/components/{comp_type}"
      }
      cache[comp_type] = Draft202012Validator(
          temp_schema, registry=self._validator._registry
      )

    validator = cache[comp_type]

Comment on lines 343 to 364
for message in messages:
if not isinstance(message, dict):
continue

components = None
surface_id = None
if "surfaceUpdate" in message: # v0.8
components = message["surfaceUpdate"].get(COMPONENTS)
surface_id = message["surfaceUpdate"].get("surfaceId")
elif "updateComponents" in message and isinstance(
if "updateComponents" in message and isinstance(
message["updateComponents"], dict
): # v0.9
):
components = message["updateComponents"].get(COMPONENTS)
surface_id = message["updateComponents"].get("surfaceId")

if components:
ref_map = extract_component_ref_fields(self._catalog)
root_id = _find_root_id(messages, surface_id)
# Always check for basic integrity (duplicates)
_validate_component_integrity(
root_id, components, ref_map, skip_root_check=not strict_integrity
)
# Always check topology (cycles), but only raise on orphans if strict_integrity is True
analyze_topology(
root_id, components, ref_map, raise_on_orphans=strict_integrity
)

_validate_recursion_and_paths(message)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This block for performing integrity and topology checks is nearly identical to the logic in the validate method (lines 292-312). To improve maintainability and reduce duplication, consider refactoring this into a shared helper method that handles component extraction and validation for both protocol versions.

Comment on lines +378 to +379
if ("Unevaluated properties are not allowed" in message or "Additional properties are not allowed" in message) and "(" in message and ")" in message:
message = message[message.find("(")+1 : message.rfind(")")]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

String-based parsing of error messages to extract property names is brittle and depends on the internal implementation details of the jsonschema library's error formatting. Consider using a regular expression or inspecting the ValidationError object's attributes for a more robust solution.

Suggested change
if ("Unevaluated properties are not allowed" in message or "Additional properties are not allowed" in message) and "(" in message and ")" in message:
message = message[message.find("(")+1 : message.rfind(")")]
if err.validator in ("additionalProperties", "unevaluatedProperties"):
match = re.search(r"\(([^)]+)\)", message)
if match:
message = match.group(1)

@wrenj wrenj requested a review from nan-yu April 6, 2026 20:33
@nan-yu
Copy link
Copy Markdown
Collaborator

nan-yu commented Apr 6, 2026

There're also two build failures. Please fix them.

@nan-yu nan-yu merged commit 15ee789 into google:main Apr 7, 2026
11 checks passed
@github-project-automation github-project-automation bot moved this from Todo to Done in A2UI Apr 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants