openai/openai-python · error · RuntimeError
Unexpected JSON data type, {type(json_data)}, cannot merge w
Error message
Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body` What it means
The OpenAI Python SDK raises this ValueError when await client.vector_stores.file_batches.retrieve() (async) is called with a falsy vector_store_id. It is a client-side pre-flight check so the GET /vector_stores/{vector_store_id}/file_batches/{batch_id} request is never sent with an invalid path segment.
Source
Thrown at src/openai/_base_client.py:538
*,
retries_taken: int = 0,
) -> httpx2.Request:
# Request bodies, files, URLs, and custom options can contain private data.
log.debug(
"Building HTTP request: method=%s retries_taken=%i",
get_http_method_for_logging(options.method),
retries_taken,
)
kwargs: dict[str, Any] = {}
json_data = options.json_data
if options.extra_json is not None:
if json_data is None:
json_data = cast(Body, options.extra_json)
elif is_mapping(json_data):
json_data = _merge_mappings(json_data, options.extra_json)
else:
raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`")
headers = self._build_headers(options, retries_taken=retries_taken)
params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params)
content_type = headers.get("Content-Type")
files = options.files
# If the given Content-Type header is multipart/form-data then it
# has to be removed so that httpx can generate the header with
# additional information for us as it has to be in this form
# for the server to be able to correctly parse the request:
# multipart/form-data; boundary=---abc--
if content_type is not None and content_type.startswith("multipart/form-data"):
if "boundary" not in content_type:
# only remove the header if the boundary hasn't been explicitly set
# as the caller doesn't want httpx to come up with their own boundary
headers.pop("Content-Type")
# As we are now sending multipart/form-data instead of application/jsonView on GitHub (pinned to 9917c6e28e)
Solutions
- Fetch and pass the actual 'vs_...' store identifier; verify the variable is populated before the retrieve call.
- Use dict.get() carefully: distinguish a missing key from an empty value and re-fetch the store ID if absent.
- In polling loops, validate both IDs once before entering the loop.
Example fix
# before
batch = await client.vector_stores.file_batches.retrieve(vector_store_id=job["store"], batch_id=job["batch"]) # job["store"] == ""
# after
vs = job.get("store") or (await client.vector_stores.create(name="docs")).id
batch = await client.vector_stores.file_batches.retrieve(vector_store_id=vs, batch_id=job["batch"]) Defensive patterns
Strategy: validation
Validate before calling
vs = job.get("vector_store_id")
if not vs:
raise ValueError("job record is missing vector_store_id")
batch = await client.vector_stores.file_batches.retrieve(vector_store_id=vs, batch_id=job["batch_id"]) Type guard
def is_valid_vector_store_id(v: object) -> bool:
return isinstance(v, str) and v.startswith("vs_") and len(v) > 3 Prevention
- Validate both IDs once before entering a polling loop.
- Treat missing dict keys as errors, not empty defaults.
When it happens
Trigger: Awaiting retrieve(vector_store_id='', batch_id='vsfb_...') or vector_store_id=None on the async resource, e.g. when polling batch status with a store ID that came back empty.
Common situations: Polling loops that read IDs from dictionaries with missing keys, deserialized job records with absent store fields, or tests using placeholder empty strings.
Related errors
- max_retries cannot be None. If you want to disable retries,
- Expected query input to be a dictionary for multipart reques
- Passing both `content` and `json_data` is not supported
- Pagination is only supported with mappings
- No next page expected; please check `.has_next_page()` befor
AI-assisted analysis of openai/openai-python@9917c6e28e (2026-08-28).
Data as JSON: /api/errors/8e7f0a8f04e70187.
Report an issue: GitHub.