Skip to content

Commit 8bb404a

Browse files
feat: override OauthConnectorInputSpecification in protocol model to preserve scopes fields
Override OauthConnectorInputSpecification and cascade through OAuthConfigSpecification -> AdvancedAuth -> ConnectorSpecification -> AirbyteMessage to ensure serpyco_rs preserves scopes, optional_scopes, and scopes_join_strategy fields during deserialization. Also convert ScopesJoinStrategy enum to string value in spec.py before passing to ConnectorSpecificationSerializer.load(). Fixes: airbytehq/airbyte-internal-issues#15963 Co-Authored-By: aldo.gonzalez@airbyte.io <aldo.gonzalez@airbyte.io>
1 parent 767a875 commit 8bb404a

3 files changed

Lines changed: 67 additions & 1 deletion

File tree

airbyte_cdk/models/airbyte_protocol.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,70 @@ class AirbyteStateMessage:
7575
destinationStats: Optional[AirbyteStateStats] = None # type: ignore [name-defined]
7676

7777

78+
# The following dataclasses have been redeclared to include scopes, optional_scopes,
79+
# and scopes_join_strategy fields that are used by declarative OAuth connectors.
80+
# The protocol model (OauthConnectorInputSpecification) does not include these fields,
81+
# so serpyco_rs silently drops them during deserialization. By overriding the model here
82+
# and cascading through OAuthConfigSpecification → AdvancedAuth → ConnectorSpecification,
83+
# the fields are preserved in the connector's spec output.
84+
# This follows the same override pattern used above for AirbyteStateBlob.
85+
@dataclass
86+
class OauthConnectorInputSpecification:
87+
consent_url: str
88+
access_token_url: str
89+
scope: Optional[str] = None
90+
scopes: Optional[List[Dict[str, Any]]] = None
91+
optional_scopes: Optional[List[Dict[str, Any]]] = None
92+
scopes_join_strategy: Optional[str] = None
93+
access_token_headers: Optional[Dict[str, Any]] = None
94+
access_token_params: Optional[Dict[str, Any]] = None
95+
extract_output: Optional[List[str]] = None
96+
state: Optional[State] = None # type: ignore [name-defined]
97+
client_id_key: Optional[str] = None
98+
client_secret_key: Optional[str] = None
99+
scope_key: Optional[str] = None
100+
state_key: Optional[str] = None
101+
auth_code_key: Optional[str] = None
102+
redirect_uri_key: Optional[str] = None
103+
token_expiry_key: Optional[str] = None
104+
105+
106+
@dataclass
107+
class OAuthConfigSpecification:
108+
oauth_user_input_from_connector_config_specification: Optional[Dict[str, Any]] = None
109+
oauth_connector_input_specification: Optional[OauthConnectorInputSpecification] = None
110+
complete_oauth_output_specification: Optional[Dict[str, Any]] = None
111+
complete_oauth_server_input_specification: Optional[Dict[str, Any]] = None
112+
complete_oauth_server_output_specification: Optional[Dict[str, Any]] = None
113+
114+
115+
@dataclass
116+
class AdvancedAuth:
117+
auth_flow_type: Optional[AuthFlowType] = None # type: ignore [name-defined]
118+
predicate_key: Optional[List[str]] = None
119+
predicate_value: Optional[str] = None
120+
oauth_config_specification: Optional[OAuthConfigSpecification] = None
121+
122+
123+
@dataclass
124+
class ConnectorSpecification:
125+
connectionSpecification: Dict[str, Any]
126+
documentationUrl: Optional[str] = None
127+
changelogUrl: Optional[str] = None
128+
supportsIncremental: Optional[bool] = None
129+
supportsNormalization: Optional[bool] = False
130+
supportsDBT: Optional[bool] = False
131+
supported_destination_sync_modes: Optional[List[DestinationSyncMode]] = None # type: ignore [name-defined]
132+
authSpecification: Optional[AuthSpecification] = None # type: ignore [name-defined]
133+
advanced_auth: Optional[AdvancedAuth] = None
134+
protocol_version: Optional[str] = None
135+
136+
78137
@dataclass
79138
class AirbyteMessage:
80139
type: Type # type: ignore [name-defined]
81140
log: Optional[AirbyteLogMessage] = None # type: ignore [name-defined]
82-
spec: Optional[ConnectorSpecification] = None # type: ignore [name-defined]
141+
spec: Optional[ConnectorSpecification] = None
83142
connectionStatus: Optional[AirbyteConnectionStatus] = None # type: ignore [name-defined]
84143
catalog: Optional[AirbyteCatalog] = None # type: ignore [name-defined]
85144
record: Optional[AirbyteRecordMessage] = None # type: ignore [name-defined]

airbyte_cdk/sources/declarative/spec/spec.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ def generate_spec(self) -> ConnectorSpecification:
5555
obj["documentationUrl"] = self.documentation_url
5656
if self.advanced_auth:
5757
self.advanced_auth.auth_flow_type = self.advanced_auth.auth_flow_type.value # type: ignore # We know this is always assigned to an AuthFlow which has the auth_flow_type field
58+
# Convert scopes_join_strategy enum to its string value (same pattern as auth_flow_type above)
59+
oauth_spec = getattr(self.advanced_auth, "oauth_config_specification", None)
60+
if oauth_spec:
61+
oauth_input = getattr(oauth_spec, "oauth_connector_input_specification", None)
62+
if oauth_input and hasattr(oauth_input, "scopes_join_strategy") and oauth_input.scopes_join_strategy is not None:
63+
oauth_input.scopes_join_strategy = oauth_input.scopes_join_strategy.value # type: ignore
5864
# Map CDK AuthFlow model to protocol AdvancedAuth model
5965
obj["advanced_auth"] = self.advanced_auth.dict()
6066

unit_tests/sources/declarative/spec/test_spec.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
oauth_connector_input_specification=model_declarative_oauth_connector_input_spec(
132132
consent_url="https://domain.host.com/endpoint/oauth?{client_id_key}={{client_id_key}}&{redirect_uri_key}={urlEncoder:{{redirect_uri_key}}}&{state_key}={{state_key}}",
133133
scope="reports:read campaigns:read",
134+
scopes_join_strategy="space", # default from component schema, preserved through protocol override
134135
access_token_headers={"Content-Type": "application/json"},
135136
access_token_params={"{auth_code_key}": "{{auth_code_key}}"},
136137
access_token_url="https://domain.host.com/endpoint/v1/oauth2/access_token/",

0 commit comments

Comments
 (0)