From 1282ba3575d79db73ade9558f4f85ffac0c0a379 Mon Sep 17 00:00:00 2001 From: Michael Broshi Date: Mon, 10 Nov 2025 20:32:17 -0500 Subject: [PATCH 1/3] Update v2 array parameter serialization to use indexed format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change array parameter serialization for v2 endpoints to use indexed format (e.g., ?include[0]=foo&include[1]=bar) instead of the repeated parameter format (e.g., ?include=foo&include=bar). This aligns v2 behavior with v1 for consistency. Changes: - Modified _encode.py _api_encode to always use indexed format - Updated tests to expect indexed format for v2 arrays 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Committed-By-Agent: claude --- stripe/_encode.py | 3 ++- tests/test_http_client.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/stripe/_encode.py b/stripe/_encode.py index 8ad5f484f..218e8ef3e 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -37,7 +37,8 @@ def _api_encode( yield (key, value.stripe_id) elif isinstance(value, list) or isinstance(value, tuple): for i, sv in enumerate(value): - encoded_key = key if api_mode == "V2" else "%s[%d]" % (key, i) + # Always use indexed format for arrays + encoded_key = "%s[%d]" % (key, i) if isinstance(sv, dict): subdict = _encode_nested_dict(encoded_key, sv) for k, v in _api_encode(subdict, api_mode): diff --git a/tests/test_http_client.py b/tests/test_http_client.py index 2f183498c..697ffd888 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -1194,8 +1194,8 @@ def test_encode_v2_array(self): values = [t for t in _api_encode(body, "V2")] - assert ("foo[dob][month]", 1) in values - assert ("foo[name]", "bat") in values + assert ("foo[0][dob][month]", 1) in values + assert ("foo[0][name]", "bat") in values class TestHTTPXClient(ClientTestBase): From d16587439f6172ada6daa91ff767d6a99c1849e8 Mon Sep 17 00:00:00 2001 From: Michael Broshi Date: Tue, 11 Nov 2025 11:56:01 -0500 Subject: [PATCH 2/3] Remove unneeded api_mode arg --- stripe/_api_requestor.py | 2 +- stripe/_encode.py | 8 +++----- stripe/_multipart_data_generator.py | 2 +- stripe/_oauth.py | 2 +- stripe/_oauth_service.py | 2 +- tests/test_api_requestor.py | 4 ++-- tests/test_http_client.py | 6 +++--- 7 files changed, 12 insertions(+), 14 deletions(-) diff --git a/stripe/_api_requestor.py b/stripe/_api_requestor.py index 4c4e44cb6..52f0f3162 100644 --- a/stripe/_api_requestor.py +++ b/stripe/_api_requestor.py @@ -600,7 +600,7 @@ def _args_for_request_with_retries( **params, } - encoded_params = urlencode(list(_api_encode(params or {}, api_mode))) + encoded_params = urlencode(list(_api_encode(params or {}))) # Don't use strict form encoding by changing the square bracket control # characters back to their literals. This is fine by the server, and diff --git a/stripe/_encode.py b/stripe/_encode.py index 218e8ef3e..73f284965 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -27,9 +27,7 @@ def _json_encode_date_callback(value): return value -def _api_encode( - data, api_mode: Optional[str] -) -> Generator[Tuple[str, Any], None, None]: +def _api_encode(data) -> Generator[Tuple[str, Any], None, None]: for key, value in data.items(): if value is None: continue @@ -41,13 +39,13 @@ def _api_encode( encoded_key = "%s[%d]" % (key, i) if isinstance(sv, dict): subdict = _encode_nested_dict(encoded_key, sv) - for k, v in _api_encode(subdict, api_mode): + for k, v in _api_encode(subdict): yield (k, v) else: yield (encoded_key, sv) elif isinstance(value, dict): subdict = _encode_nested_dict(key, value) - for subkey, subvalue in _api_encode(subdict, api_mode): + for subkey, subvalue in _api_encode(subdict): yield (subkey, subvalue) elif isinstance(value, datetime.datetime): yield (key, _encode_datetime(value)) diff --git a/stripe/_multipart_data_generator.py b/stripe/_multipart_data_generator.py index 1e0be2ba1..3151df83e 100644 --- a/stripe/_multipart_data_generator.py +++ b/stripe/_multipart_data_generator.py @@ -19,7 +19,7 @@ def __init__(self, chunk_size: int = 1028): def add_params(self, params): # Flatten parameters first - params = dict(_api_encode(params, "V1")) + params = dict(_api_encode(params)) for key, value in params.items(): if value is None: diff --git a/stripe/_oauth.py b/stripe/_oauth.py index 8f6b9d5dd..ba0214548 100644 --- a/stripe/_oauth.py +++ b/stripe/_oauth.py @@ -323,7 +323,7 @@ def authorize_url( OAuth._set_client_id(params) if "response_type" not in params: params["response_type"] = "code" - query = urlencode(list(_api_encode(params, "V1"))) + query = urlencode(list(_api_encode(params))) url = connect_api_base + path + "?" + query return url diff --git a/stripe/_oauth_service.py b/stripe/_oauth_service.py index 3744f9533..b02260a14 100644 --- a/stripe/_oauth_service.py +++ b/stripe/_oauth_service.py @@ -64,7 +64,7 @@ def authorize_url( self._set_client_id(params) if "response_type" not in params: params["response_type"] = "code" - query = urlencode(list(_api_encode(params, "V1"))) + query = urlencode(list(_api_encode(params))) # connect_api_base will be always set to stripe.DEFAULT_CONNECT_API_BASE # if it is not overridden on the client explicitly. diff --git a/tests/test_api_requestor.py b/tests/test_api_requestor.py index 3e5a80947..d2b2fd997 100644 --- a/tests/test_api_requestor.py +++ b/tests/test_api_requestor.py @@ -220,7 +220,7 @@ def test_encodes_null_values_preview(self, requestor, http_client_mock): def test_dictionary_list_encoding(self): params = {"foo": {"0": {"bar": "bat"}}} - encoded = list(_api_encode(params, "V1")) + encoded = list(_api_encode(params)) key, value = encoded[0] assert key == "foo[0][bar]" @@ -237,7 +237,7 @@ def test_ordereddict_encoding(self): ] ) } - encoded = list(_api_encode(params, "V1")) + encoded = list(_api_encode(params)) assert encoded[0][0] == "ordered[one]" assert encoded[1][0] == "ordered[two]" diff --git a/tests/test_http_client.py b/tests/test_http_client.py index 697ffd888..be0f8400f 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -1176,7 +1176,7 @@ class TestAPIEncode: def test_encode_dict(self): body = {"foo": {"dob": {"month": 1}, "name": "bat"}} - values = [t for t in _api_encode(body, "V1")] + values = [t for t in _api_encode(body)] assert ("foo[dob][month]", 1) in values assert ("foo[name]", "bat") in values @@ -1184,7 +1184,7 @@ def test_encode_dict(self): def test_encode_array(self): body = {"foo": [{"dob": {"month": 1}, "name": "bat"}]} - values = [t for t in _api_encode(body, "V1")] + values = [t for t in _api_encode(body)] assert ("foo[0][dob][month]", 1) in values assert ("foo[0][name]", "bat") in values @@ -1192,7 +1192,7 @@ def test_encode_array(self): def test_encode_v2_array(self): body = {"foo": [{"dob": {"month": 1}, "name": "bat"}]} - values = [t for t in _api_encode(body, "V2")] + values = [t for t in _api_encode(body)] assert ("foo[0][dob][month]", 1) in values assert ("foo[0][name]", "bat") in values From e395abb7e5ed5ee85a61ff8b425ea327d3795d22 Mon Sep 17 00:00:00 2001 From: Michael Broshi Date: Tue, 11 Nov 2025 11:58:41 -0500 Subject: [PATCH 3/3] Fix linting error --- stripe/_encode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stripe/_encode.py b/stripe/_encode.py index 73f284965..228edfdc6 100644 --- a/stripe/_encode.py +++ b/stripe/_encode.py @@ -2,7 +2,7 @@ import datetime import time from collections import OrderedDict -from typing import Generator, Optional, Tuple, Any +from typing import Generator, Tuple, Any def _encode_datetime(dttime: datetime.datetime):