Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions airbyte_cdk/manifest_migrations/migrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
from airbyte_cdk.manifest_migrations.migrations.http_requester_request_body_json_data_to_request_body import (
HttpRequesterRequestBodyJsonDataToRequestBody,
)
from airbyte_cdk.manifest_migrations.migrations.http_requester_request_body_plain_text_json_to_request_body_json import (
HttpRequesterRequestBodyPlainTextJsonToRequestBodyJson,
)
from airbyte_cdk.manifest_migrations.migrations.http_requester_url_base_to_url import (
HttpRequesterUrlBaseToUrl,
)

__all__ = [
"HttpRequesterPathToUrl",
"HttpRequesterRequestBodyJsonDataToRequestBody",
"HttpRequesterRequestBodyPlainTextJsonToRequestBodyJson",
"HttpRequesterUrlBaseToUrl",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
#


from airbyte_cdk.manifest_migrations.manifest_migration import (
TYPE_TAG,
ManifestMigration,
ManifestType,
)


class HttpRequesterRequestBodyPlainTextJsonToRequestBodyJson(ManifestMigration):
"""Migrate `RequestBodyPlainText` with JSON-like content to `RequestBodyJsonObject`.

The Connector Builder UI sometimes generates `request_body: {type: RequestBodyPlainText, ...}`
for raw JSON string bodies (Jinja templates containing JSON). After CDK v7.17.1 (PR #971),
`RequestBodyPlainText` is correctly routed to `request_body_data` (form-encoded), but this
broke connectors where the Builder had misclassified JSON content as plain text.

This migration detects `RequestBodyPlainText` where the value is a JSON-like string and
converts it to `RequestBodyJsonObject` with a string value. `RequestBodyJsonObject` now
accepts both dict and string values, and routes through `InterpolatedNestedRequestInputProvider`
which correctly handles string templates containing JSON.
"""

component_type = "HttpRequester"
request_body_key = "request_body"
plain_text_type = "RequestBodyPlainText"
json_object_type = "RequestBodyJsonObject"

def should_migrate(self, manifest: ManifestType) -> bool:
if manifest.get(TYPE_TAG) != self.component_type:
return False
request_body = manifest.get(self.request_body_key)
if not isinstance(request_body, dict):
return False
if request_body.get("type") != self.plain_text_type:
return False
value = request_body.get("value")
if not isinstance(value, str):
return False
return self._is_json_like(value)

def migrate(self, manifest: ManifestType) -> None:
request_body = manifest[self.request_body_key]
request_body["type"] = self.json_object_type

def validate(self, manifest: ManifestType) -> bool:
request_body = manifest.get(self.request_body_key)
if not isinstance(request_body, dict):
return False
is_json_object_with_string = (
request_body.get("type") == self.json_object_type
and isinstance(request_body.get("value"), str)
and self._is_json_like(request_body.get("value", ""))
)
is_plain_text_json = (
request_body.get("type") == self.plain_text_type
and isinstance(request_body.get("value"), str)
and self._is_json_like(request_body.get("value", ""))
)
return is_json_object_with_string and not is_plain_text_json

@staticmethod
def _is_json_like(value: str) -> bool:
"""Check if a string value looks like JSON content.

Returns `True` when the stripped value starts with `{` or `[`, excluding
Jinja expression openers (`{{`) and Jinja block openers (`{%`).
"""
stripped = value.strip()
if not stripped:
return False
if stripped.startswith("["):
return True
if (
stripped.startswith("{")
and not stripped.startswith("{{")
and not stripped.startswith("{%")
):
return True
return False
7 changes: 7 additions & 0 deletions airbyte_cdk/manifest_migrations/migrations/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,10 @@ manifest_migrations:
description: |
This migration updates the `request_body_json_data` field in the `http_requester` spec to `request_body`.
The `request_body_json_data` field is deprecated and will be removed in a future version.
- name: http_requester_request_body_plain_text_json_to_request_body_json
order: 4
description: |
This migration converts `RequestBodyPlainText` with JSON-like string content to
`RequestBodyJsonObject` with a string value. The Connector Builder UI sometimes
misclassifies JSON string bodies as plain text, which after CDK v7.17.1 routes them
to form-encoded `request_body_data` instead of JSON.
Original file line number Diff line number Diff line change
Expand Up @@ -4699,7 +4699,7 @@ definitions:
type: string
RequestBodyJsonObject:
title: Json Object Body
description: Request body value converted into a JSON object
description: Request body value converted into a JSON object. Can be a dict or a Jinja-interpolated string that evaluates to a JSON object at runtime.
type: object
required:
- type
Expand All @@ -4709,8 +4709,10 @@ definitions:
type: string
enum: [RequestBodyJsonObject]
value:
type: object
additionalProperties: true
anyOf:
- type: object
additionalProperties: true
- type: string
RequestBodyGraphQL:
title: GraphQL Body
description: Request body value converted into a GraphQL query object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1401,7 +1401,7 @@ class RequestBodyUrlEncodedForm(BaseModel):

class RequestBodyJsonObject(BaseModel):
type: Literal["RequestBodyJsonObject"]
value: Dict[str, Any]
value: Union[Dict[str, Any], str]


class RequestBodyGraphQlQuery(BaseModel):
Expand Down
237 changes: 237 additions & 0 deletions unit_tests/manifest_migrations/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,243 @@ def expected_manifest_with_migrated_to_request_body() -> Dict[str, Any]:
}


@pytest.fixture
def manifest_with_request_body_plain_text_json() -> Dict[str, Any]:
return {
"version": "0.0.0",
"type": "DeclarativeSource",
"check": {
"type": "CheckStream",
"stream_names": ["A"],
},
"definitions": {
"streams": {
"A": {
"type": "DeclarativeStream",
"name": "A",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/search",
"http_method": "POST",
"request_body": {
"type": "RequestBodyPlainText",
"value": '{"sort": [{"field": "createdAt", "order": "ASC"}], "filter": [{"type": "equals", "field": "active", "value": "true"}]}',
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {"$ref": "#/schemas/A"},
},
},
"B": {
"type": "DeclarativeStream",
"name": "B",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/query",
"http_method": "POST",
"request_body": {
"type": "RequestBodyPlainText",
"value": "plain text body that is not JSON",
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {"$ref": "#/schemas/B"},
},
},
},
},
"streams": [
{"$ref": "#/definitions/streams/A"},
{"$ref": "#/definitions/streams/B"},
],
"schemas": {
"A": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_a1": {"type": "string"}},
},
"B": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_b1": {"type": "string"}},
},
},
}


@pytest.fixture
def expected_manifest_with_plain_text_json_migrated() -> Dict[str, Any]:
return {
"version": "0.0.0",
"type": "DeclarativeSource",
"check": {"type": "CheckStream", "stream_names": ["A"]},
"definitions": {
"streams": {
"A": {
"type": "DeclarativeStream",
"name": "A",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/search",
"http_method": "POST",
"request_body": {
"type": "RequestBodyJsonObject",
"value": '{"sort": [{"field": "createdAt", "order": "ASC"}], "filter": [{"type": "equals", "field": "active", "value": "true"}]}',
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_a1": {"type": "string"}},
},
},
},
"B": {
"type": "DeclarativeStream",
"name": "B",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/query",
"http_method": "POST",
"request_body": {
"type": "RequestBodyPlainText",
"value": "plain text body that is not JSON",
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_b1": {"type": "string"}},
},
},
},
},
},
"streams": [
{
"type": "DeclarativeStream",
"name": "A",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/search",
"http_method": "POST",
"request_body": {
"type": "RequestBodyJsonObject",
"value": '{"sort": [{"field": "createdAt", "order": "ASC"}], "filter": [{"type": "equals", "field": "active", "value": "true"}]}',
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_a1": {"type": "string"}},
},
},
},
{
"type": "DeclarativeStream",
"name": "B",
"retriever": {
"type": "SimpleRetriever",
"requester": {
"type": "HttpRequester",
"url": "https://api.example.com/v1/query",
"http_method": "POST",
"request_body": {
"type": "RequestBodyPlainText",
"value": "plain text body that is not JSON",
},
},
"record_selector": {
"type": "RecordSelector",
"extractor": {"type": "DpathExtractor", "field_path": []},
},
},
"schema_loader": {
"type": "InlineSchemaLoader",
"schema": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_b1": {"type": "string"}},
},
},
},
],
"schemas": {
"A": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_a1": {"type": "string"}},
},
"B": {
"type": "object",
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": True,
"properties": {"field_b1": {"type": "string"}},
},
},
"metadata": {
"applied_migrations": [
{
"from_version": "0.0.0",
"to_version": "*",
"migration": "HttpRequesterRequestBodyPlainTextJsonToRequestBodyJson",
"migrated_at": "2025-04-01T00:00:00+00:00",
},
]
},
}


class DummyMigration(ManifestMigration):
def _process_manifest(self, manifest):
self.is_migrated = False
Expand Down
Loading
Loading