Skip to content

feat: add default parameter to JsonObject.serialize()#1508

Closed
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/json-object-serialize-default
Closed

feat: add default parameter to JsonObject.serialize()#1508
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/json-object-serialize-default

Conversation

@waiho-gumloop
Copy link

Summary

Add an optional default parameter to JsonObject.serialize() that is passed through to json.dumps(). This allows callers to handle custom types (e.g., datetime) that are not natively JSON-serializable, using the standard json.dumps extension mechanism.

Fixes #1507

Changes

  • google/cloud/spanner_v1/data_types.py: Add default=None parameter to JsonObject.serialize(), passed to all json.dumps() call sites (dict, array, and scalar branches).
  • tests/unit/test_datatypes.py: Add 5 unit tests covering:
    • Dict with datetime values + custom default
    • Array with datetime values + custom default
    • Scalar datetime value + custom default
    • TypeError raised without default (existing behavior preserved)
    • default=None produces identical output to current behavior

Backward Compatibility

Fully backward-compatible. default=None is the default, which matches the existing json.dumps() behavior. No existing callers are affected.

Motivation

JsonObject.serialize() currently cannot handle non-standard types in JSON columns. This is a common need — for example, datetime objects in JSON columns raise TypeError. The standard Python way to extend JSON serialization is via json.dumps(obj, default=fn), and this PR simply plumbs that parameter through.

This also unblocks the python-spanner-sqlalchemy dialect from supporting the standard SQLAlchemy create_engine(json_serializer=...) parameter (see context in #1507).

Made with Cursor

Add an optional `default` parameter to `JsonObject.serialize()` that is
passed through to `json.dumps()`. This allows callers to handle custom
types (e.g., `datetime`) that are not natively JSON-serializable, using
the standard `json.dumps` extension mechanism.

Backward-compatible: `default=None` preserves existing behavior.
Made-with: Cursor
@waiho-gumloop waiho-gumloop requested review from a team as code owners February 28, 2026 00:16
@product-auto-label product-auto-label bot added the size: m Pull request size is medium. label Feb 28, 2026
@product-auto-label product-auto-label bot added the api: spanner Issues related to the googleapis/python-spanner API. label Feb 28, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the JsonObject.serialize() method by introducing a default parameter, mirroring the functionality of Python's standard json.dumps(). This crucial addition enables the serialization of custom data types, such as datetime objects, within JSON columns, which previously caused TypeError exceptions. By plumbing this parameter through, the change significantly improves the flexibility and utility of JsonObject for handling complex data structures, and is fully backward-compatible.

Highlights

  • JsonObject.serialize() method enhancement: The serialize method now accepts an optional default parameter, which is directly passed to json.dumps() to enable serialization of custom, non-natively JSON-serializable types.
  • Custom type serialization support: This change allows users to provide a custom function to handle types like datetime objects during JSON serialization, addressing a common limitation and unblocking python-spanner-sqlalchemy integration.
  • Comprehensive unit testing: New unit tests were added to validate the default parameter's functionality across various scenarios, including dictionaries, arrays, scalar values, and proper error handling when no default is provided for custom types.
Changelog
  • google/cloud/spanner_v1/data_types.py
    • Modified JsonObject.serialize() to accept an optional default callable argument.
    • Updated all internal calls to json.dumps() within serialize() to pass the new default parameter.
    • Expanded the docstring for serialize() to explain the default parameter and provide an example for datetime objects.
  • tests/unit/test_datatypes.py
    • Added a new test class Test_JsonObject_serialize_default.
    • Included tests for serializing dictionaries with custom types using default.
    • Included tests for serializing arrays with custom types using default.
    • Included a test to confirm TypeError is raised when custom types are present without a default function.
    • Included a test to verify that default=None maintains existing serialization behavior.
    • Included a test for serializing scalar custom types with default.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a default parameter to JsonObject.serialize() for custom JSON serialization, which is a useful enhancement. The implementation correctly passes the parameter to json.dumps(), and the new unit tests provide good coverage for various scenarios. My feedback focuses on improving the API documentation to promote safer usage patterns and on enhancing the test code's maintainability by reducing duplication.

Comment on lines +94 to +95
obj.serialize(default=lambda o: o.isoformat()
if hasattr(o, 'isoformat') else str(o))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example default function can lead to subtle bugs. The else str(o) clause will convert any unhandled non-serializable object into its string representation (e.g., '<MyObject object at 0x...>'), which might not be the desired behavior and can mask serialization errors. A more robust approach is to handle only the expected types and let other types raise a TypeError, which is the standard behavior of json.dumps.

Consider providing a more explicit example that promotes this safer pattern, even if it's more verbose:

# In a scope where `datetime` is imported
def custom_serializer(obj):
    if isinstance(obj, (datetime.date, datetime.datetime)):
        return obj.isoformat()
    raise TypeError(f'Object of type {type(obj).__name__} is not JSON serializable')

obj.serialize(default=custom_serializer)

dt = datetime(2023, 6, 15, 9, 30, 0)
data = {"ts": dt, "name": "test"}
obj = JsonObject(data)
result = obj.serialize(default=lambda o: o.isoformat() if isinstance(o, datetime) else str(o))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This lambda function is repeated in test_array_with_custom_type_and_default and test_scalar_with_default. To improve maintainability and readability, consider defining it once at the test class level and reusing it across these tests.

For example:

class Test_JsonObject_serialize_default(unittest.TestCase):
    _DATETIME_SERIALIZER = lambda o: o.isoformat() if isinstance(o, datetime) else str(o)

    def test_dict_with_custom_type_and_default(self):
        # ...
        result = obj.serialize(default=self._DATETIME_SERIALIZER)
        # ...

@waiho-gumloop
Copy link
Author

Closing this PR — after further analysis, we found a cleaner approach for the SQLAlchemy dialect that doesn't require modifying JsonObject.serialize().

Instead of injecting a default parameter into the serialization pipeline, the dialect can use a serialize-then-wrap pattern: pre-serialize the Python value using the user's custom json_serializer (handling custom types like datetime), then create a JsonObject from the resulting JSON string via JsonObject.from_str(). The JsonObject then only contains native Python types, so the existing bare serialize() works without modification.

This removes the dependency on any core library changes and keeps JsonObject untouched. The fix will live entirely in python-spanner-sqlalchemy.

The default parameter addition is still a reasonable enhancement on its own for direct users of JsonObject, but it's no longer blocking any downstream work. Closing for now to keep scope minimal — can be revisited if there's independent demand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the googleapis/python-spanner API. size: m Pull request size is medium.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JsonObject.serialize() does not support custom JSON serialization for non-standard types

2 participants