fix(openapi): resolve $refs under patternProperties for map schemas - #19383
Conversation
Leftover component refs in those JSON Schema keywords caused jsonref to fail with Unresolvable JSON pointer, so affected endpoints were ingested with no schema. Promote patternProperties to additionalProperties so map-like response bodies still get columns. Co-authored-by: Cursor <cursoragent@cursor.com>
PR SummaryOverview OpenAPI resolution ( JSON schema typing ( Reviewed by Cursor Bugbot for commit 5b3089d. Bugbot is set up for automated code reviews on this repo. Configure here. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Do not set additionalProperties when named properties already exist (or arrive via allOf), since json_schema_util treats dict additionalProperties as a map and drops those fields. Promote after allOf merge instead. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed the Bugbot / cubic review notes:
|
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Treat empty properties as map-only for promotion, merge patternProperties/propertyNames through allOf, and resolve those keywords after the merge so nested $refs are expanded. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
… allOf Same-pattern entries and multiple propertyNames constraints from allOf members are combined under allOf instead of overwriting earlier schemas. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Combine colliding same-pattern patternProperties and repeated propertyNames contributors under a single flattened allOf via a shared helper, so a third-or-later member is appended as a sibling instead of being nested (merge_allof_schemas does not expand nested member allOf, which dropped earlier schemas). Also preserve sibling constraints (minLength, pattern, ...) next to an allOf when resolving propertyNames. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…onstraints Resolve $refs in sibling keywords (anyOf, $ref, ...) that sit next to a propertyNames allOf instead of copying them verbatim, and carry the JSON-Schema validation keywords (minLength, maxLength, pattern, ...) through merge_allof_schemas so colliding same-pattern value schemas keep their constraints. Add interaction tests covering 3+ members with a $ref. Co-authored-by: Cursor <cursoragent@cursor.com>
…schema_from_endpoint The Args section was left listing only endpoint_spec/sw_dict after the previous cycle added endpoint_k/method for warning context.
…n-dict property/items value - _normalize_map_schemas_list (used for oneOf/anyOf/allOf) didn't sanitize a bare boolean member the way _normalize_map_schemas_mapping/items already did, so a "oneOf": [true, ...] survived to get_schema_metadata and crashed with the same TypeError the prior cycle's fix targeted for properties/items -- destroying the endpoint's entire spec-derived schema. When every member of a union keyword normalizes away (all "false"), the keyword itself is now dropped rather than left as a dangling empty list. - Broaden all three normalization sites (properties/patternProperties values, items, and oneOf/anyOf/allOf members) from an identity check on True/False to a shape check (isinstance(..., dict)): get_schema_metadata crashes identically on None or a scalar, not just a bare bool, and None is a realistic case (a hand-written "foo:" with no value parses to None). - Add regression tests for all of the above, verified to fail against the prior commit via git-stash discrimination.
…ion, nullable maps, non-UTF8 bodies - ensure_only_one_token now uses truthiness (matching get_swagger's own check) instead of `is not None`, so an empty SecretStr token doesn't falsely count as configured alongside get_token. - _process_endpoint wraps schema extraction in its own try/except so a failure there degrades in place instead of discarding the dataset init workunits already produced earlier in the same generator (the caller materializes the whole generator with list() before yielding). - JsonSchemaTranslator._get_type_from_schema now checks additionalProperties for list-form types resolving to "object" (e.g. OpenAPI 3.1's `type: [object, null]`), so nullable maps resolve to MapTypeClass instead of silently dropping their fields. - extract_fields also catches UnicodeDecodeError, which json.loads raises directly (not as JSONDecodeError) for a body that isn't valid text in its detected encoding. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…_util.py Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
…perties/additionalProperties regression test The last commit's applied suggestion left a duplicate `):` in _get_type_from_schema, an unmatched-parenthesis SyntaxError breaking every consumer of json_schema_util (including the openapi source). Remove the duplicate line. Also add the regression test the accompanying review comment asked for: a nullable object declaring both named `properties` and a catchall `additionalProperties` must stay classified as an object, not a map, since the map branch only emits a single value-type field and never walks `properties` -- misclassifying it would silently drop the named fields. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
max-datahub
left a comment
There was a problem hiding this comment.
Review
Thanks for digging into the map-schema extraction gap — the core fix is correct and the malformed-input hardening comes from a real place. My main feedback is about scope: the titled bug fix is a small fraction of a ~1,770-line source rewrite (+3,900 lines of tests) that bundles several unrelated concerns, and a good chunk of the added complexity has no effect on what this connector actually emits. I'd like to see it split before merge. Details below, grouped by structural concerns → correctness nits → tests, with the good parts first.
What's working well
- The titled fix is right. Resolving
$refs underpatternProperties/propertyNamesand promoting map-only schemas toadditionalProperties, plus thejson_schema_utillist-form-type map handling (type: ["object","null"]→map), is the correct shape for the "map bodies ingest with no columns" bug. - Removing
warnings.catch_warningsis a genuine fix, not just a refactor. The parser emits zerowarnings.warncalls, so the old block only caught incidental library warnings and then didw.message.args[0].split(" --- ")[1]— a latentIndexErroron any warning without that separator. Good to see it gone. get_tokcredential-leak hardening is careful and correct.raise ... from None+ status-only messages genuinely prevent the password-in-URL (GET token flow) from surfacing throughreport.failure's verbatim exception rendering. This deserves to land.- Request timeouts (30s), the shared-
sw_dictmutation-safety copies (avoiding cross-endpoint corruption), and switchingreport_bad_responsesoffraise-on-unmapped-code to graceful degradation are all correct.
Structural concerns
1. Scope / reviewability. The PR bundles at least four independent changes under a fix(openapi): resolve $refs under patternProperties title: (a) the map/$ref fix, (b) an allOf-merge engine rewrite, (c) a broad hardening sweep (auth typing, SecretStr truthiness semantics, timeouts, get_token/forced_examples validation), and (d) the parser→report warning bridge. Each carries independent regression risk for a widely-used connector, and a bug in any of them ships under an innocuous title with no clean way to bisect. Recommend splitting — at minimum peel the get_tok security fix and the report bridge into their own PRs; the actual reported bug is fixable in a small fraction of this diff.
2. The numeric/validation allOf-merge logic (~130 lines) has no effect on connector output. _merge_allof_validation_keywords, _merge_bound_with_exclusivity, _merge_exclusive_bound, _apply_bool_exclusivity, _merge_numeric_keyword, _ALLOF_NUMERIC_BOUNDS merge minimum/maximum/exclusive*/minLength/maxItems/multipleOf/uniqueItems. The sole consumer of this resolution path, json_schema_util.get_schema_metadata, reads none of these keywords — it emits fields from type/properties/additionalProperties/items/oneOf/anyOf/allOf/enum only. So all of this intricate OpenAPI-3.0-boolean-vs-draft-6-numeric exclusivity handling changes only the cosmetic platformSchema.rawSchema blob, never the navigable fields. That's a large, edge-case-dense surface to maintain for zero observable behavior. Suggest dropping it and keeping only the merges that feed extraction (properties/required/items/additionalProperties/patternProperties/oneOf/anyOf).
3. The parser→report warning bridge (~70 lines) is a lot of concurrency machinery, and has a correctness gap. _RefcountedLevelOverride (lock + refcount + saved-level) plus the per-thread-filtered _CollectingLogHandler defend against multiple APISource runs sharing the module logger concurrently — which the sequential ingestion pipeline doesn't do. More importantly, _CollectingLogHandler.emit binds to the thread that first advances the get_workunits_internal generator; if a consumer ever resumes that generator on a different thread, every parser warning is silently dropped from the report — the exact bridging this code exists to provide. Simpler options, cheapest first: rely on the module logger like every other free function in openapi_parser already does, or give the handful of parser functions an optional collector/report argument.
Correctness inconsistencies (lower blast radius)
json_schema_util._get_type_from_schema— the two map-detection branches diverge. The new list-form branch gates onand not schema.get("properties"); the existing string-form branch (type: "object") does not. So a hybrid schema with bothpropertiesand dictadditionalPropertiesis classified"map"whentype: "object"(named fields dropped) but"object"whentype: ["object","null"](map value dropped) — same logical schema, opposite output depending only on nullability. The inline comment claiming it does "the same map check as the plain-string-type branch below" is inaccurate. Please align the two branches and fix the comment._ALLOF_FIRST_WINS_KEYWORDSapplies first-wins totypeandenum. Under allOf,enumshould intersect ([1,2,3]∩[2,3,4]=[2,3]), not take the first member. Descriptive-only here so low impact, but semantically wrong for a legal composition.stringify_forced_examplescoercesboolviastr()→"True"/"False". The field doc says "bool is accepted via int" (implying"1"), butstr(True) == "True", which most APIs expecting a boolean path param will reject. It's also untested (the added tests cover only int→"1"and null-rejection).schema_resolution_max_depthgainsge=1, le=100— a config-validation tightening that rejects previously-accepted recipes (0, or>100) but isn't listed in theupdating-datahub.mdbreaking-changes entry (which correctly documents theget_token/forced_exampleschanges). Low severity, but please document it or relax the bound.- Double warning on schema-extraction failure. In
_process_endpoint, when the extractiontryraises, theexceptemits "Schema Extraction Failed" and setsschema_metadata=None; the subsequent falsy-schema branch then also incrementsno_schema_foundand emits "No Schema Extracted" for the same endpoint — two slightly contradictory report entries for one failure.
Test suite
There are ~187 new test functions here, and while the core ones are strong, the suite is inflated in three ways that mirror the source concerns:
- ~13 test dead or speculative code. The ~11 numeric-bound merge tests assert on
minLength/maximum/exclusiveMaximumvalues thatjson_schema_utilnever reads (test_merge_allof_integer_bounds_preserve_int_typeliterally assertsmax(3,5)returns anint— a language feature). The two_capture_parser_warningsthread tests exist only to cover the log bridge above. If concerns #2 and #3 are addressed, these delete for free. - ~45 are near-duplicates that should be parametrized (→ ~12). The clearest: the 9
test_get_endpoints_malformed_*_does_not_abort_other_endpointstests are byte-identical except for where the malformed value sits (@pytest.mark.parametrizeover(spec, expected)); the 7 bool-subschema-collision tests exercise the same_intersect_subschemassemantics across three keyword slots; the 8normalize_bare_boolean_*/normalize_none_*tests apply the same rule per slot; the 4check_sw_version_*tests vary only the version string. - A handful are trivial plumbing — e.g.
test_request_call_forwards_proxies_on_*mock-assert that a kwarg reachesrequests.get, andtest_get_swag_json_parses_json_responsecovers the happy path ofjson.loads.
The genuinely valuable tests — mutation-safety (*_does_not_mutate_shared_component*), credential-leak prevention, circular-ref/depth-cap termination, oneOf/anyOf sibling preservation, per-endpoint isolation, and the core patternProperties resolution — are excellent and should stay. A focused ~55–70 tests would give equivalent regression protection.
(Minor style note, for the file rather than this PR: metadata-ingestion's AGENTS.md asks for pytest + bare assert rather than unittest.TestCase/self.assertEqual; the new tests match the existing TestCase classes in the file, which is a reasonable consistency call.)
Suggested path forward
- Land the map/
$reffix (+ thejson_schema_utilbranch alignment) as a small, focused PR. - Land the
get_tokcredential-leak fix separately. - Drop the numeric-bound merge engine and the threaded log bridge.
- Fold the malformed-input hardening in on its own, with the near-duplicate tests parametrized.
Happy to look again once it's split — the underlying fix is solid and worth getting in.
- Claude (on behalf of Max)
…and address review nits
- json_schema_util: detect typeless maps (a schema-valued additionalProperties
with no type) and unify map detection via _is_map_schema so the string-form
`type: object` and list-form `type: [object, null]` branches no longer
diverge on schemas that also declare named properties.
- openapi_parser: normalize a non-dict `items` (false/null/scalar) to an empty
schema {} instead of dropping it -- a missing `items` is treated as `true`
and would silently mistype the array as an array of strings.
- openapi_parser: intersect `enum` across allOf members instead of first-wins.
- openapi: coerce bool forced_examples via int ("1"/"0"), not str(True).
- openapi: don't emit a second "No Schema Extracted" warning after an
extraction failure already reported "Schema Extraction Failed".
- docs: note the schema_resolution_max_depth 1..100 bound in updating-datahub.
- tests for each of the above.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks @max-datahub — really thorough review. Latest push (
Each has a focused unit test. On the structural feedback (split the PR; drop the numeric-bound
Let me know which and I'll turn it around. |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Thanks for the fast turnaround @acrylJonny — I went through
One small leftover: One thing worth surfacing before merge: the
Both look like correctness improvements to me, but they change a widely-shared module under a On the structural choice: I'd go with Option 1 — carve this down to the map/ Thanks again — the core fix is solid and I'm keen to see it land.
|
|
Thanks @max-datahub — good call flagging the shared-module blast radius. I confirmed against
No golden-file drift. I ran every source that imports
Worth noting the kafka integration golden fixtures ( On the On the structural split (Option 1) + deleting the numeric-bound |
…chema_util alignment Per review (Option 1), reduce this PR to the titled fix only: - openapi_parser: resolve $refs under patternProperties/propertyNames and promote map-only patternProperties schemas to additionalProperties so json_schema_util extracts the map value type; enum under allOf now intersects members instead of first-wins. - json_schema_util: unify map detection via _is_map_schema across the string-form, list-form, and typeless branches (shared-extractor correctness fix, verified against kafka/kinesis/confluent/json-schema tests). Peeled into follow-up PRs (preserved on openapi-full-hardening-snapshot): get_token typed config, forced_examples validation, schema_resolution_max_depth bound, the numeric-bound allOf merge engine, the parser->report bridge, and the broader malformed-input hardening. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Went with Option 1 and carved this PR down to the core, pushed in
Everything else is peeled out and preserved on branch To your shared-extractor concern: I ran every source that imports The |
|
Follow-ups are up, stacked so each shows only its own diff:
Per your ask, the follow-ups do not relocate the numeric-bound All three (this PR + both follow-ups) are green on |
|
Follow-up stack is now complete — with the third PR, the stack reproduces this branch's original (pre-strip) content exactly, minus the two pieces called out in review:
Deliberately dropped (per review), and not in any of the three:
Verified by diffing the combined stack tip against a preserved snapshot of the original full branch: the only source differences are those two removals. Breaking recipe-validation changes are documented in |
max-datahub
left a comment
There was a problem hiding this comment.
Approving the carved-down core. I re-read the full net diff (decb902) and it's exactly the right scope:
json_schema_util—_is_map_schemaunifies the string-form / list-form / typeless branches: the hybridproperties+ dictadditionalPropertiescase now consistently resolves toobject(named fields preserved), and a typelessadditionalPropertiesmap resolves tomap. It's a shared-module change, but you ran theJsonSchemaTranslatorconsumers (json-schema / kinesis / kafka / confluent) green, and the kafka golden fixtures are Avro-routed (avro_util, notJsonSchemaTranslator), so no drift. 👍openapi_parser—$refresolution underpatternProperties/propertyNamesplus map-only promotion toadditionalProperties(after theallOfmerge, so named properties win), andenumintersection acrossallOfmembers. Copy-before-write mutation-safety is preserved throughout.- Seven focused unit tests, one per distinct behavior — no bloat.
Confirmed the numeric-bound allOf merge engine and the threaded parser→report bridge are absent from this PR (and from the whole follow-up stack — I grepped all four). This is a minimal, self-contained delta on master rather than the original rewrite, with the rest cleanly staged in #19905 / #19906 / #19907.
Non-blocking: the diff currently shows a couple of unrelated updating-datahub.md entries (#19549 / #19554) picked up from a master merge — pure base skew, they'll collapse on squash-merge. A rebase would give a cleaner diff but isn't required.
Nice work on the split — the core fix is tight. Reviewing #19907 next.
- Claude (on behalf of Max)
…a valid Intersecting `enum` across allOf members can collapse to an empty set for disjoint members. Emitting `enum: []` is invalid per the JSON Schema meta-schema (minItems 1), so json_schema_util's check_schema rejects the whole resolved schema and drops every field for that endpoint -- not just the enum constraint. The composition is unsatisfiable, so drop the enum keyword and let the field stay typed by its other keywords. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 5b3089d. Configure here.
| merged_schema, sw_dict, resolving_refs=True, max_depth=max_depth | ||
| ) | ||
|
|
||
| # A disjoint enum intersection collapses to []. Emitting `enum: []` is invalid |
There was a problem hiding this comment.
allOf drops patternProperties maps
Medium Severity
merge_allof_schemas folds properties and additionalProperties from each allOf member but never copies patternProperties. Promotion then runs only on the merged result, so a map defined solely inside allOf (including the common allOf: [{$ref: ...}] form) never becomes additionalProperties and still extracts with no columns.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 5b3089d. Configure here.


Summary
patternProperties, leaving#/components/schemas/...refs unresolved so map-like response bodies ingested with no columns.patternProperties(andpropertyNames, so leftover$refs cannot breakjsonref), merge allOf map/validation keywords safely, and promote toadditionalPropertieswhen needed sojson_schema_utilcan extract fields.get_token/ coercedforced_examples, no vestigialcatch_warningscrash path, safe OpenAPI prerelease version parsing, and report-visible schema conversion failures (swallow_exceptions=False).Test plan
patternProperties/ allOf merge / promotion /propertyNames$ref/ prerelease version / per-endpoint isolation /forced_examples/get_token./gradlew :metadata-ingestion:lintand:metadata-ingestion:testSingle -PtestFile=tests/unit/test_openapi.pyFollow-ups (out of scope)
dataPlatformInstanceaspect (standards gap; pre-existing)