Skip to content

fix(declarative): stop coercing static scalar strings in request_body_json - #1081

Draft
Airbyte Support (Airbyte-Support) wants to merge 2 commits into
mainfrom
devin/1785206396-nested-mapping-static-strings
Draft

fix(declarative): stop coercing static scalar strings in request_body_json#1081
Airbyte Support (Airbyte-Support) wants to merge 2 commits into
mainfrom
devin/1785206396-nested-mapping-static-strings

Conversation

@Airbyte-Support

@Airbyte-Support Airbyte Support (Airbyte-Support) commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

A Connector Builder user reported that RequestBodyJsonObject with clientId: "478" sends {"clientId": 478} — a number — and that no amount of YAML quoting or Jinja ("{{ '478' }}") fixes it, breaking an API that requires a string.

Cause: every string in request_body_json is routed through JinjaInterpolation.eval, which runs ast.literal_eval on the rendered output. For a static YAML literal there is nothing to render, so the only effect of that round-trip is type coercion. The result looks arbitrary to users: in the reporter's own manifest, storeId: ["478-1", "478-2", ...] survives (not valid Python literals) while clientId: "478" becomes 478, and "None" becomes None and is then dropped from the body entirely.

The naive fix — skip literal_eval for all static strings — is too broad, and CI caught why: source-intercom deliberately encodes structure as a string and depends on it being parsed.

sort: "{\"field\": \"updated_at\", \"order\": \"ascending\"}"   # must stay a dict

So the rule is narrowed to structured vs. scalar: a static string keeps its literal-eval result only when that result is a dict or list; scalars are returned verbatim. Encoding a JSON object/array as a string is a deliberate, meaningful use of literal_eval; scalar coercion never is.

# InterpolatedNestedMapping._eval
if "{{" not in value and "{%" not in value:
    evaluated = self._interpolation.eval(value, ...)
    return evaluated if isinstance(evaluated, (dict, list)) else value
input before after
"478" 478 "478"
"0012" "0012" "0012"
"None" dropped from body "None"
'{"field": "updated_at"}' {"field": ...} {"field": ...}
"[1, 2, 3]" [1, 2, 3] [1, 2, 3]
"{{ next_page_token['offset'] }}" int int

The change is confined to InterpolatedNestedMapping, which is only wired into InterpolatedNestedRequestInputProvider (request body JSON). jinja.py and InterpolatedString are deliberately untouched — changing _literal_eval globally would affect request params, headers, paths, and every other interpolation caller.

Rollout risk: a static quoted scalar number in request_body_json now stays a string. A scan of every declarative manifest in airbytehq/airbyte found exactly one affected value: source-looker manifest.yaml:26, request_body_json.limit: "10000", which becomes "10000". No quoted "True"/"False"/"None" values exist anywhere.

Test plan

  • Parametrized cases in test_interpolated_nested_mapping.py: scalars preserved (as dict keys, nested in dicts, inside lists), structured JSON object/array strings still parsed, a regression case mirroring intercom's sort, and "{{ 1 + 1 }}" still evaluating to 2.
  • Case in test_interpolated_request_options_provider.py asserting RequestBodyJsonObject(value={"clientId": "478"}) yields {"clientId": "478"}.
  • ruff, mypy, pytest unit_tests/sources/declarative, and all 27 CI checks pass — including source-intercom, which fails on the un-narrowed version.

Reported by Syed Khadeer.

Link to Devin session: https://app.devin.ai/sessions/ba9cc31475fb453fba21067b02845c73
Requested by: Airbyte Support (@Airbyte-Support)

…d of coercing them

Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

Copy link
Copy Markdown

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1785206396-nested-mapping-static-strings#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1785206396-nested-mapping-static-strings

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

PyTest Results (Fast)

4 144 tests  +11   4 132 ✅ +11   7m 53s ⏱️ +18s
    1 suites ± 0      12 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit ac74d0a. ± Comparison against base commit 7e96546.

This pull request removes 13 and adds 24 tests. Note that renamed tests count towards both.
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_boolean_false_value-input_request_json5-expected_request_json5]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_graphql_query-input_request_json12-expected_request_json12]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_integer_falsy_value-input_request_json6-expected_request_json6]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_interpolated_keys-input_request_json4-expected_request_json4]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_nested_objects-input_request_json10-expected_request_json10]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_nested_objects_interpolated keys-input_request_json11-expected_request_json11]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_none_value-input_request_json9-expected_request_json9]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_number_falsy_value-input_request_json7-expected_request_json7]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_static_json-input_request_json0-expected_request_json0]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_string_falsy_value-input_request_json8-expected_request_json8]
…
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[jinja_value_still_coerces]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_digit_only_key]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_digit_only_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_intercom_sort_json]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_leading_zero_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_none_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_string_nested_in_dict]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_strings_nested_in_list]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_structured_dict]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_structured_list]
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown

PyTest Results (Full)

4 147 tests  +11   4 135 ✅ +11   12m 35s ⏱️ +12s
    1 suites ± 0      12 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit ac74d0a. ± Comparison against base commit 7e96546.

This pull request removes 13 and adds 24 tests. Note that renamed tests count towards both.
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_boolean_false_value-input_request_json5-expected_request_json5]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_graphql_query-input_request_json12-expected_request_json12]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_integer_falsy_value-input_request_json6-expected_request_json6]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_interpolated_keys-input_request_json4-expected_request_json4]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_nested_objects-input_request_json10-expected_request_json10]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_nested_objects_interpolated keys-input_request_json11-expected_request_json11]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_none_value-input_request_json9-expected_request_json9]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_number_falsy_value-input_request_json7-expected_request_json7]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_static_json-input_request_json0-expected_request_json0]
unit_tests.sources.declarative.requesters.request_options.test_interpolated_request_options_provider ‑ test_interpolated_request_json_using_request_body[test_string_falsy_value-input_request_json8-expected_request_json8]
…
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[jinja_value_still_coerces]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_digit_only_key]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_digit_only_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_intercom_sort_json]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_leading_zero_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_none_string]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_string_nested_in_dict]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_strings_nested_in_list]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_structured_dict]
unit_tests.sources.declarative.interpolation.test_interpolated_nested_mapping ‑ test_static_strings_are_preserved_and_jinja_values_are_interpolated[static_structured_list]
…

♻️ This comment has been updated with latest results.

Co-Authored-By: syed.khadeer@airbyte.io <cloud-support@airbyte.io>
@devin-ai-integration devin-ai-integration Bot changed the title fix(declarative): stop coercing static strings in request_body_json to other types fix(declarative): stop coercing static scalar strings in request_body_json Jul 28, 2026
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.

1 participant