_to_json.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. # Copyright (c) 2025-2026 Buf Technologies, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from __future__ import annotations
  15. import json
  16. import math
  17. from base64 import b64encode
  18. from dataclasses import dataclass
  19. from typing import TYPE_CHECKING, Any
  20. from ._descriptors import (
  21. DescEnum,
  22. DescFieldValueEnum,
  23. DescFieldValueList,
  24. DescFieldValueMap,
  25. DescFieldValueMessage,
  26. DescFieldValueScalar,
  27. DescMessage,
  28. ScalarType,
  29. SupportedFieldPresence,
  30. )
  31. from ._oneof import Oneof
  32. from ._typing import JsonPrimitive, JsonValue, assert_never
  33. from ._validate import validate
  34. from ._wkt_registry import is_null_value_enum, match_wkt
  35. if TYPE_CHECKING:
  36. from types import NotImplementedType
  37. from ._message import Message
  38. from ._registry import Registry
  39. from .wkt import Struct, Value
  40. @dataclass(slots=True, frozen=True)
  41. class ToJsonOptions:
  42. always_emit_implicit: bool
  43. print_enums_as_ints: bool
  44. use_proto_field_name: bool
  45. registry: Registry | None = None
  46. def _struct_to_json_value(value: Struct, opts: ToJsonOptions) -> JsonValue:
  47. return {key: _value_to_json_value(val, opts) for key, val in value.fields.items()}
  48. def _value_to_json_value(value: Value, opts: ToJsonOptions) -> JsonValue:
  49. match value.kind:
  50. case Oneof(field="null_value"):
  51. return None
  52. case Oneof(field="number_value", value=v):
  53. # Value does not allow non-finite numbers.
  54. if not math.isfinite(v):
  55. msg = "value cannot be NaN or Infinity"
  56. raise ValueError(msg)
  57. return _scalar_to_json_value(ScalarType.DOUBLE, v)
  58. case Oneof(field="string_value", value=v) | Oneof(field="bool_value", value=v):
  59. return v
  60. case Oneof(field="struct_value", value=v):
  61. return _struct_to_json_value(v, opts)
  62. case Oneof(field="list_value", value=v):
  63. return [_value_to_json_value(element, opts) for element in v.values]
  64. case _:
  65. msg = "value must have exactly one field set"
  66. raise ValueError(msg)
  67. def _scalar_to_json_value(scalar_type: ScalarType, value: Any) -> JsonPrimitive:
  68. match scalar_type:
  69. case (
  70. ScalarType.INT64
  71. | ScalarType.UINT64
  72. | ScalarType.SINT64
  73. | ScalarType.FIXED64
  74. | ScalarType.SFIXED64
  75. ):
  76. return str(value)
  77. case ScalarType.FLOAT | ScalarType.DOUBLE:
  78. if math.isnan(value):
  79. return "NaN"
  80. if math.isinf(value):
  81. return "Infinity" if value > 0 else "-Infinity"
  82. return value
  83. case ScalarType.BYTES:
  84. return b64encode(value).decode()
  85. case _:
  86. return value
  87. def _enum_to_json_value(
  88. desc_enum: DescEnum, value: int, opts: ToJsonOptions
  89. ) -> JsonValue:
  90. if is_null_value_enum(desc_enum):
  91. return None
  92. if opts.print_enums_as_ints:
  93. return int(value)
  94. if enum_value := desc_enum._values_by_number.get(value):
  95. return enum_value.name
  96. if not desc_enum.open:
  97. msg = f"invalid enum value {value} for enum {desc_enum.type_name}"
  98. raise ValueError(msg)
  99. # If the enum is open but value is unknown, print the integer value.
  100. return value
  101. def _container_value_to_json_value( # noqa: RET503
  102. desc_element: ScalarType | DescEnum | DescMessage, value: Any, opts: ToJsonOptions
  103. ) -> JsonValue:
  104. match desc_element:
  105. case ScalarType():
  106. return _scalar_to_json_value(desc_element, value)
  107. case DescEnum():
  108. return _enum_to_json_value(desc_element, value, opts)
  109. case DescMessage():
  110. return _message_to_json_value(value, opts)
  111. case _:
  112. assert_never(desc_element)
  113. def _try_wkt_to_json(
  114. msg: Message, opts: ToJsonOptions
  115. ) -> JsonValue | NotImplementedType:
  116. """Encode a well-known type to its special JSON representation.
  117. Returns the JSON value if the message was handled, NotImplemented if
  118. generic encoding should proceed.
  119. """
  120. wkt = match_wkt(type(msg)._desc)
  121. if wkt is None:
  122. return NotImplemented
  123. return wkt.to_json_value(msg, opts)
  124. def _message_to_json_value(message: Message, opts: ToJsonOptions) -> JsonValue:
  125. if (json_value := _try_wkt_to_json(message, opts)) is not NotImplemented:
  126. return json_value
  127. # Regular fields
  128. result: dict[str, JsonValue] = {}
  129. for desc_field in message._desc.fields:
  130. if not message._contains_member(desc_field) and (
  131. not opts.always_emit_implicit
  132. or desc_field.presence != SupportedFieldPresence.IMPLICIT
  133. ):
  134. continue
  135. value = message._get_member(desc_field)
  136. match field_value := desc_field.value:
  137. case DescFieldValueScalar():
  138. json_value = _scalar_to_json_value(field_value.scalar, value)
  139. case DescFieldValueEnum():
  140. json_value = _enum_to_json_value(field_value.enum, value, opts)
  141. case DescFieldValueMessage():
  142. json_value = _message_to_json_value(value, opts)
  143. case DescFieldValueList():
  144. json_value = [
  145. _container_value_to_json_value(field_value.element, element, opts)
  146. for element in value
  147. ]
  148. case DescFieldValueMap():
  149. json_value = {
  150. (
  151. key if isinstance(key, str) else json.dumps(key)
  152. ): _container_value_to_json_value(field_value.value, val, opts)
  153. for key, val in value.items()
  154. }
  155. case _:
  156. assert_never(field_value)
  157. json_key = (
  158. desc_field.name if opts.use_proto_field_name else desc_field.json_name
  159. )
  160. result[json_key] = json_value
  161. # Extension fields
  162. if opts.registry and (uf := message._unknown_fields):
  163. for field_number in uf:
  164. ext_desc = opts.registry.extension_for(message._desc, field_number)
  165. if not ext_desc:
  166. continue
  167. value = message[ext_desc.type]
  168. match field_value := ext_desc.value:
  169. case DescFieldValueScalar():
  170. json_value = _scalar_to_json_value(field_value.scalar, value)
  171. case DescFieldValueEnum():
  172. json_value = _enum_to_json_value(field_value.enum, value, opts)
  173. case DescFieldValueMessage():
  174. json_value = _message_to_json_value(value, opts)
  175. case DescFieldValueList():
  176. json_value = [
  177. _container_value_to_json_value(
  178. field_value.element, element, opts
  179. )
  180. for element in value
  181. ]
  182. case _:
  183. assert_never(field_value)
  184. result[f"[{ext_desc.type_name}]"] = json_value
  185. return result
  186. def to_json(message: Message, opts: ToJsonOptions) -> str:
  187. return json.dumps(_message_to_json_value(message, opts))
  188. def message_to_json_value(
  189. message: Message,
  190. /,
  191. *,
  192. registry: Registry | None = None,
  193. always_emit_implicit: bool = False,
  194. print_enums_as_ints: bool = False,
  195. use_proto_field_name: bool = False,
  196. ) -> JsonValue:
  197. """Converts a protobuf message to a Python value that can be encoded as JSON.
  198. This can be useful when embedding a message within a larger structure encoded as JSON.
  199. The message content is converted using ProtoJSON semantics, including support for
  200. well-known-types and extensions if a registry is provided.
  201. See [`message_from_json_value`][] for the inverse operation.
  202. Examples:
  203. ```python
  204. user = User(
  205. name="Alice", created_at=Timestamp.from_datetime(datetime(2024, 1, 1))
  206. )
  207. # For example, a request to a task runner
  208. request = {"task": "save_user", "user": message_to_json_value(user)}
  209. assert (
  210. json.dumps(request)
  211. == '{"task": "save_user", "user": {"name": "Alice", "createdAt": "2024-01-01T00:00:00Z"}}'
  212. )
  213. ```
  214. """
  215. validate(message)
  216. opts = ToJsonOptions(
  217. always_emit_implicit=always_emit_implicit,
  218. print_enums_as_ints=print_enums_as_ints,
  219. use_proto_field_name=use_proto_field_name,
  220. registry=registry,
  221. )
  222. return _message_to_json_value(message, opts)