diff --git a/src/betterproto/__init__.py b/src/betterproto/__init__.py index 62056e3..3f36a48 100644 --- a/src/betterproto/__init__.py +++ b/src/betterproto/__init__.py @@ -1260,7 +1260,12 @@ class Message(ABC): setattr(self, field_name, v) return self - def to_json(self, indent: Union[None, int, str] = None) -> str: + def to_json( + self, + indent: Union[None, int, str] = None, + include_default_values: bool = False, + casing: Casing = Casing.CAMEL, + ) -> str: """A helper function to parse the message instance into its JSON representation. @@ -1273,12 +1278,24 @@ class Message(ABC): indent: Optional[Union[:class:`int`, :class:`str`]] The indent to pass to :func:`json.dumps`. + include_default_values: :class:`bool` + If ``True`` will include the default values of fields. Default is ``False``. + E.g. an ``int32`` field will be included with a value of ``0`` if this is + set to ``True``, otherwise this would be ignored. + + casing: :class:`Casing` + The casing to use for key values. Default is :attr:`Casing.CAMEL` for + compatibility purposes. + Returns -------- :class:`str` The JSON representation of the message. """ - return json.dumps(self.to_dict(), indent=indent) + return json.dumps( + self.to_dict(include_default_values=include_default_values, casing=casing), + indent=indent, + ) def from_json(self: T, value: Union[str, bytes]) -> T: """A helper function to return the message instance from its JSON diff --git a/tests/test_features.py b/tests/test_features.py index b59bfe8..2deef2b 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -1,3 +1,4 @@ +import json from copy import ( copy, deepcopy, @@ -190,6 +191,37 @@ def test_json_casing(): assert test == CasingTest(1, 2, 3, 4) + # Serializing should be strict. + assert json.loads(test.to_json()) == { + "pascalCase": 1, + "camelCase": 2, + "snakeCase": 3, + "kabobCase": 4, + } + + assert json.loads(test.to_json(casing=betterproto.Casing.SNAKE)) == { + "pascal_case": 1, + "camel_case": 2, + "snake_case": 3, + "kabob_case": 4, + } + + +def test_dict_casing(): + @dataclass + class CasingTest(betterproto.Message): + pascal_case: int = betterproto.int32_field(1) + camel_case: int = betterproto.int32_field(2) + snake_case: int = betterproto.int32_field(3) + kabob_case: int = betterproto.int32_field(4) + + # Parsing should accept almost any input + test = CasingTest().from_dict( + {"PascalCase": 1, "camelCase": 2, "snake_case": 3, "kabob-case": 4} + ) + + assert test == CasingTest(1, 2, 3, 4) + # Serializing should be strict. assert test.to_dict() == { "pascalCase": 1, @@ -233,6 +265,37 @@ def test_optional_flag(): assert Request().parse(b"\n\x00").flag is False +def test_to_json_default_values(): + @dataclass + class TestMessage(betterproto.Message): + some_int: int = betterproto.int32_field(1) + some_double: float = betterproto.double_field(2) + some_str: str = betterproto.string_field(3) + some_bool: bool = betterproto.bool_field(4) + + # Empty dict + test = TestMessage().from_dict({}) + + assert json.loads(test.to_json(include_default_values=True)) == { + "someInt": 0, + "someDouble": 0.0, + "someStr": "", + "someBool": False, + } + + # All default values + test = TestMessage().from_dict( + {"someInt": 0, "someDouble": 0.0, "someStr": "", "someBool": False} + ) + + assert json.loads(test.to_json(include_default_values=True)) == { + "someInt": 0, + "someDouble": 0.0, + "someStr": "", + "someBool": False, + } + + def test_to_dict_default_values(): @dataclass class TestMessage(betterproto.Message):