_from_json.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  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 math
  16. from base64 import b64decode
  17. from dataclasses import dataclass
  18. from json import loads as parse_json
  19. from typing import TYPE_CHECKING, Literal, TypeVar, cast
  20. from ._descriptors import (
  21. DescEnum,
  22. DescExtension,
  23. DescField,
  24. DescFieldValueEnum,
  25. DescFieldValueList,
  26. DescFieldValueMap,
  27. DescFieldValueMessage,
  28. DescFieldValueScalar,
  29. DescFieldValueSingular,
  30. DescMessage,
  31. DescOneof,
  32. ScalarType,
  33. )
  34. from ._oneof import Oneof
  35. from ._typing import JsonValue, assert_never
  36. from ._validate import (
  37. FLOAT32_MAX,
  38. FLOAT32_MIN,
  39. INT32_MAX,
  40. INT32_MIN,
  41. INT64_MAX,
  42. INT64_MIN,
  43. UINT32_MAX,
  44. UINT64_MAX,
  45. )
  46. from ._wkt_registry import (
  47. WktListValue,
  48. WktStruct,
  49. WktValue,
  50. is_null_value_enum,
  51. is_wkt_value,
  52. match_wkt,
  53. )
  54. if TYPE_CHECKING:
  55. from ._enum import Enum
  56. from ._message import Message
  57. from ._registry import Registry
  58. from .wkt import ListValue, NullValue, Struct, Value
  59. T = TypeVar("T", bound=Message)
  60. @dataclass(slots=True, frozen=True)
  61. class FromJsonOptions:
  62. ignore_unknown_fields: bool
  63. registry: Registry | None
  64. def merge_from_json(
  65. message: Message,
  66. json: str | bytes | bytearray,
  67. *,
  68. ignore_unknown_fields: bool = False,
  69. registry: Registry | None = None,
  70. ) -> None:
  71. """Parse a ProtoJSON string, merging fields into an existing message.
  72. Merge rules by field kind:
  73. - Scalar and enum: the existing value is overwritten.
  74. - Message: recursively merged if already present, otherwise set.
  75. - Repeated: elements are appended.
  76. - Map: entries are added; existing keys are overwritten. Message-valued map entries are not merged.
  77. Args:
  78. message: The message instance to merge into.
  79. json: A str, bytes, or bytearray instance containing the ProtoJSON.
  80. ignore_unknown_fields: If `True`, unknown fields in the JSON data are
  81. silently discarded instead of raising an error.
  82. registry: Required to read google.protobuf.Any and extensions from
  83. JSON format.
  84. Raises:
  85. json.JSONDecodeError: If json_source is not valid JSON.
  86. TypeError: If the JSON structure does not match expected types.
  87. ValueError: If a google.protobuf.Any or an extension cannot be resolved
  88. through the registry.
  89. """
  90. json_value = parse_json(json, object_pairs_hook=_no_duplicates)
  91. opts = FromJsonOptions(
  92. ignore_unknown_fields=ignore_unknown_fields, registry=registry
  93. )
  94. _read_message(message, json_value, opts)
  95. def _read_message(msg: Message, json: JsonValue, opts: FromJsonOptions) -> None:
  96. if _try_wkt_from_json(msg, json, opts):
  97. return
  98. if not isinstance(json, dict):
  99. err = f"cannot decode {msg.__class__.__qualname__} from JSON: {json}"
  100. raise TypeError(err)
  101. desc = msg.__class__._desc
  102. seen_oneofs = dict[DescOneof, DescField]()
  103. seen_fields = dict[DescField, str]()
  104. for key, value in json.items():
  105. field = desc._fields_by_json_name.get(key)
  106. if field:
  107. if seen := seen_fields.get(field):
  108. err = f"field set multiple times by {seen} and {key}"
  109. raise ValueError(err)
  110. seen_fields[field] = key
  111. field_value = field.value
  112. if isinstance(field_value, DescFieldValueSingular) and field_value.oneof:
  113. if value is None and isinstance(field_value, DescFieldValueScalar):
  114. continue
  115. seen = seen_oneofs.get(field_value.oneof)
  116. if seen:
  117. err = f"oneof set multiple times by {seen.name} and {field.name}"
  118. raise ValueError(err)
  119. seen_oneofs[field_value.oneof] = field
  120. _read_field(msg, field, value, opts)
  121. else:
  122. extension = None
  123. if (
  124. key.startswith("[")
  125. and key.endswith("]")
  126. and opts.registry
  127. and (extension := opts.registry.extension(key[1:-1]))
  128. and extension.extendee.type_name == desc.type_name
  129. ):
  130. _read_extension(msg, extension, value, opts)
  131. if not extension and not opts.ignore_unknown_fields:
  132. err = (
  133. f"cannot decode {desc.type_name} from JSON: key: '{key}' is unknown"
  134. )
  135. raise ValueError(err)
  136. def _read_field(
  137. msg: Message, field: DescField, json: JsonValue, opts: FromJsonOptions
  138. ) -> None:
  139. match field_value := field.value:
  140. case DescFieldValueScalar():
  141. _read_scalar_field(msg, field, field_value, json)
  142. case DescFieldValueEnum():
  143. _read_enum_field(msg, field, field_value, json, opts)
  144. case DescFieldValueMessage():
  145. _read_message_field(msg, field, field_value, json, opts)
  146. case DescFieldValueList():
  147. _read_list_field(msg._get_member(field), field, field_value, json, opts)
  148. case DescFieldValueMap():
  149. _read_map_field(msg._get_member(field), field, field_value, json, opts)
  150. case _:
  151. assert_never(field_value)
  152. def _read_extension(
  153. msg: Message, ext: DescExtension, json: JsonValue, opts: FromJsonOptions
  154. ) -> None:
  155. match field_value := ext.value:
  156. case DescFieldValueScalar():
  157. _read_scalar_extension(msg, ext, field_value, json)
  158. case DescFieldValueEnum():
  159. _read_enum_extension(msg, ext, field_value, json, opts)
  160. case DescFieldValueMessage():
  161. _read_message_extension(msg, ext, field_value, json, opts)
  162. case DescFieldValueList():
  163. _read_list_extension(msg, ext, field_value, json, opts)
  164. case _:
  165. assert_never(field_value)
  166. def _read_scalar_extension(
  167. msg: Message, ext: DescExtension, field_value: DescFieldValueScalar, json: JsonValue
  168. ) -> None:
  169. if json is None:
  170. del msg[ext.type]
  171. else:
  172. msg[ext.type] = _read_scalar(ext, field_value.scalar, json)
  173. def _read_enum_extension(
  174. msg: Message,
  175. ext: DescExtension,
  176. field_value: DescFieldValueEnum,
  177. json: JsonValue,
  178. opts: FromJsonOptions,
  179. ) -> None:
  180. if _is_resetting_null(field_value.enum, json):
  181. del msg[ext.type]
  182. else:
  183. value = _read_enum(field_value.enum, json, opts.ignore_unknown_fields)
  184. if value is not None:
  185. msg[ext.type] = value
  186. def _read_message_extension(
  187. msg: Message,
  188. ext: DescExtension,
  189. field_value: DescFieldValueMessage,
  190. json: JsonValue,
  191. opts: FromJsonOptions,
  192. ) -> None:
  193. if _is_resetting_null(field_value.message, json):
  194. del msg[ext.type]
  195. else:
  196. value = field_value.message.type()
  197. _read_message(value, json, opts)
  198. msg[ext.type] = value
  199. def _read_list_extension(
  200. msg: Message,
  201. ext: DescExtension,
  202. field_value: DescFieldValueList,
  203. json: JsonValue,
  204. opts: FromJsonOptions,
  205. ) -> None:
  206. if json is None:
  207. return
  208. if not isinstance(json, list):
  209. raise _field_error(ext, f"expected list got {type(json)}", TypeError)
  210. msg[ext.type] = [
  211. v
  212. for element in json
  213. if (v := _read_container_item(ext, field_value.element, element, opts))
  214. is not None
  215. ]
  216. def _read_scalar_field(
  217. msg: Message, field: DescField, field_value: DescFieldValueScalar, json: JsonValue
  218. ) -> None:
  219. if json is None:
  220. msg._del_member(field)
  221. return
  222. msg._set_member(field, _read_scalar(field, field_value.scalar, json))
  223. def _read_enum_field(
  224. msg: Message,
  225. field: DescField,
  226. field_value: DescFieldValueEnum,
  227. json: JsonValue,
  228. opts: FromJsonOptions,
  229. ) -> None:
  230. if _is_resetting_null(field_value.enum, json):
  231. msg._del_member(field)
  232. return
  233. value = _read_enum(field_value.enum, json, opts.ignore_unknown_fields)
  234. if value is not None:
  235. msg._set_member(field, value)
  236. def _read_list_field(
  237. list_: list[object],
  238. field: DescField,
  239. field_value: DescFieldValueList,
  240. json: JsonValue,
  241. opts: FromJsonOptions,
  242. ) -> None:
  243. if json is None:
  244. return
  245. if not isinstance(json, list):
  246. raise _field_error(field, f"expected list got {type(json)}", TypeError)
  247. list_.extend(
  248. value
  249. for element in json
  250. if (value := _read_container_item(field, field_value.element, element, opts))
  251. is not None
  252. )
  253. def _read_map_field(
  254. dict_: dict[str | bool | int, object],
  255. field: DescField,
  256. field_value: DescFieldValueMap,
  257. json: JsonValue,
  258. opts: FromJsonOptions,
  259. ) -> None:
  260. if json is None:
  261. return
  262. if not isinstance(json, dict):
  263. raise _field_error(field, f"expected dict got {type(json)}", TypeError)
  264. for json_key, json_value in json.items():
  265. key = _read_map_key(field, field_value, json_key)
  266. value = _read_container_item(field, field_value.value, json_value, opts)
  267. if value is not None:
  268. dict_[key] = value
  269. def _read_message_field(
  270. msg: Message,
  271. field: DescField,
  272. field_value: DescFieldValueMessage,
  273. json: JsonValue,
  274. opts: FromJsonOptions,
  275. ) -> None:
  276. if _is_resetting_null(field_value.message, json):
  277. msg._del_member(field)
  278. return
  279. value = (
  280. msg._get_member(field)
  281. if msg._contains_member(field)
  282. else field_value.message.type()
  283. )
  284. _read_message(value, json, opts)
  285. msg._set_member(field, value)
  286. def _read_map_key(
  287. field: DescField, field_value: DescFieldValueMap, json: JsonValue
  288. ) -> bool | int | str:
  289. match field_value.key:
  290. case ScalarType.BOOL:
  291. if json == "true":
  292. return True
  293. if json == "false":
  294. return False
  295. raise _field_error(field, f"unexpected bool map key value {json}")
  296. case ScalarType.STRING:
  297. return _read_string(field, json)
  298. case (
  299. ScalarType.DOUBLE | ScalarType.FLOAT | ScalarType.BYTES
  300. ): # This is because the Map key is not narrow enough
  301. msg = f"invalid map key type: {field_value.key}"
  302. raise AssertionError(msg)
  303. case _:
  304. return _read_int(field, field_value.key, json)
  305. def _read_container_item(
  306. field: DescField | DescExtension,
  307. element: ScalarType | DescMessage | DescEnum,
  308. json: JsonValue,
  309. opts: FromJsonOptions,
  310. ) -> bool | int | float | str | bytes | Message | Enum | None:
  311. if isinstance(element, ScalarType) and json is not None:
  312. return _read_scalar(field, element, json)
  313. if isinstance(element, DescMessage) and not _is_resetting_null(element, json):
  314. msg = element.type()
  315. _read_message(msg, json, opts)
  316. return msg
  317. if isinstance(element, DescEnum) and not _is_resetting_null(element, json):
  318. return _read_enum(element, json, opts.ignore_unknown_fields)
  319. raise _field_error(
  320. field,
  321. f"unexpected null value for {'map value' if isinstance(field, DescField) and isinstance(field.value, DescFieldValueMap) else 'list item'}",
  322. )
  323. def _read_enum(
  324. desc: DescEnum,
  325. json: JsonValue,
  326. ignore_unknown_fields: bool, # noqa: FBT001
  327. ) -> Enum | None:
  328. if json is None:
  329. return desc.type(desc.values[0].number)
  330. if not isinstance(json, bool) and isinstance(
  331. json, int
  332. ): # isinstance(bool_value, int) is True
  333. if value := desc._values_by_number.get(json):
  334. return desc.type(value.number)
  335. if ignore_unknown_fields:
  336. return None
  337. # Succeeds for open enum, raises an error for closed
  338. return desc.type(json)
  339. if isinstance(json, str):
  340. if value := desc._values_by_name.get(json):
  341. return desc.type(value.number)
  342. if ignore_unknown_fields:
  343. return None
  344. msg = f"cannot decode {desc.type_name} from JSON: {json}"
  345. raise ValueError(msg)
  346. def _read_scalar(
  347. desc: DescField | DescExtension, scalar_type: ScalarType, json: JsonValue
  348. ) -> bool | int | float | str | bytes:
  349. match scalar_type:
  350. case ScalarType.BOOL:
  351. if not isinstance(json, bool):
  352. raise _field_error(
  353. desc, f"unexpected json type: {type(json)}", TypeError
  354. )
  355. return json
  356. case ScalarType.FLOAT:
  357. v = _parse_float(desc, json)
  358. if math.isfinite(v) and not FLOAT32_MIN <= v <= FLOAT32_MAX:
  359. raise _field_error(
  360. desc, f"float value out of range: {json}", OverflowError
  361. )
  362. return v
  363. case ScalarType.DOUBLE:
  364. return _parse_float(desc, json)
  365. case ScalarType.STRING:
  366. return _read_string(desc, json)
  367. case ScalarType.BYTES:
  368. if not isinstance(json, str):
  369. raise _field_error(
  370. desc, f"expected base64-encoded string got: {type(json)}", TypeError
  371. )
  372. # Accept standard and URL-safe base64, adding padding if necessary
  373. altchars = "-_" if "-" in json or "_" in json else None
  374. if padding := len(json) % 4:
  375. json = json + (4 - padding) * "="
  376. try:
  377. return b64decode(json, altchars=altchars, validate=True)
  378. except ValueError:
  379. raise _field_error(desc, "invalid base64 data", ValueError) from None
  380. case _:
  381. return _read_int(desc, scalar_type, json)
  382. def _read_int(
  383. desc: DescField | DescExtension,
  384. int_type: Literal[
  385. ScalarType.INT32,
  386. ScalarType.SINT32,
  387. ScalarType.SFIXED32,
  388. ScalarType.INT64,
  389. ScalarType.SINT64,
  390. ScalarType.SFIXED64,
  391. ScalarType.UINT32,
  392. ScalarType.FIXED32,
  393. ScalarType.UINT64,
  394. ScalarType.FIXED64,
  395. ],
  396. json: JsonValue,
  397. ) -> int:
  398. v = _parse_int(desc, json)
  399. match int_type:
  400. case ScalarType.INT32 | ScalarType.SINT32 | ScalarType.SFIXED32:
  401. if not INT32_MIN <= v < INT32_MAX:
  402. raise _field_error(
  403. desc, f"value {v} out of range for int32", OverflowError
  404. )
  405. case ScalarType.INT64 | ScalarType.SINT64 | ScalarType.SFIXED64:
  406. if not INT64_MIN <= v < INT64_MAX:
  407. raise _field_error(
  408. desc, f"value {v} out of range for int64", OverflowError
  409. )
  410. case ScalarType.UINT32 | ScalarType.FIXED32:
  411. if not 0 <= v < UINT32_MAX:
  412. raise _field_error(
  413. desc, f"value {v} out of range for uint32", OverflowError
  414. )
  415. case ScalarType.UINT64 | ScalarType.FIXED64:
  416. if not 0 <= v < UINT64_MAX:
  417. raise _field_error(
  418. desc, f"value {v} out of range for uint64", OverflowError
  419. )
  420. case _:
  421. assert_never(int_type)
  422. return v
  423. def _read_string(desc: DescField | DescExtension, json: JsonValue) -> str:
  424. if not isinstance(json, str):
  425. raise _field_error(desc, f"expected string got: {type(json)}", TypeError)
  426. try:
  427. # Raises UnicodeEncodeError for lone surrogates - they are not permitted in Protobuf strings
  428. json.encode("utf-8")
  429. except UnicodeEncodeError:
  430. raise _field_error(desc, f"invalid utf-8 string in field {desc}") from None
  431. return json
  432. def _parse_int(desc: DescField | DescExtension, json: JsonValue) -> int:
  433. if not isinstance(json, bool) and isinstance(
  434. json, int
  435. ): # isinstance(bool_value, int) is True
  436. return json
  437. if isinstance(json, float):
  438. f = json
  439. elif isinstance(json, str):
  440. if not json or json.strip() != json:
  441. raise _field_error(desc, f"invalid integer value: {json}")
  442. try:
  443. return int(json, 10) # Default base parses any valid Python literal
  444. except ValueError:
  445. try:
  446. f = float(json) # For strings that are "x.0"
  447. except ValueError:
  448. raise _field_error(desc, f"invalid integer value: '{json}'") from None
  449. else:
  450. raise _field_error(desc, f"unexpected json type: {type(json)}", TypeError)
  451. if not f.is_integer():
  452. raise _field_error(desc, f"expected integer, got non-integer float: {json}")
  453. return int(f)
  454. def _parse_float(desc: DescField | DescExtension, json: JsonValue) -> float:
  455. if not isinstance(json, bool) and isinstance(
  456. json, int
  457. ): # isinstance(bool_value, int) is True
  458. return float(json)
  459. if isinstance(json, float):
  460. f = json
  461. elif isinstance(json, str):
  462. if json in ("Infinity", "-Infinity", "NaN"):
  463. return float(json)
  464. if not json or json.strip() != json:
  465. raise _field_error(desc, f"invalid float/double value: {json}")
  466. try:
  467. f = float(json)
  468. except ValueError:
  469. raise _field_error(desc, f"invalid float/double value: {json}") from None
  470. else:
  471. raise _field_error(desc, f"unexpected json type: {type(json)}", TypeError)
  472. if not math.isfinite(f):
  473. raise _field_error(desc, "unexpected infinite/NaN number")
  474. return f
  475. def _field_error(
  476. desc: DescField | DescExtension, msg: str, typ: type[Exception] = ValueError
  477. ) -> Exception:
  478. return (
  479. typ(f"{msg} for extension {desc.type_name}")
  480. if isinstance(desc, DescExtension)
  481. else typ(f"{msg} for field {desc.parent.type_name}.{desc.name}")
  482. )
  483. def _is_resetting_null(element: DescMessage | DescEnum, json: JsonValue) -> bool:
  484. """Whether a JSON null should clear the field rather than set a value.
  485. All message and enum fields treat null as "clear", except
  486. google.protobuf.Value (where null represents NullValue) and
  487. google.protobuf.NullValue (where null is the literal enum value).
  488. """
  489. if json is not None:
  490. return False
  491. if isinstance(element, DescMessage):
  492. return not is_wkt_value(element)
  493. return not is_null_value_enum(element)
  494. def _try_wkt_from_json(msg: Message, json: JsonValue, opts: FromJsonOptions) -> bool:
  495. """Decode a well-known type from its special JSON representation.
  496. Returns True if the message was handled, False if generic parsing should proceed.
  497. """
  498. wkt = match_wkt(msg.__class__._desc)
  499. if wkt is None:
  500. return False
  501. return wkt.from_json(msg, json, opts)
  502. def _struct_from_json(
  503. msg: Struct, json: JsonValue, opts: FromJsonOptions, fields_desc: DescFieldValueMap
  504. ) -> None:
  505. if not isinstance(json, dict):
  506. err = f"cannot decode {msg._desc.type_name} from JSON: {json}"
  507. raise TypeError(err)
  508. value_desc = fields_desc.value
  509. assert isinstance(value_desc, DescMessage) # noqa: S101
  510. value_wkt = match_wkt(value_desc)
  511. assert isinstance(value_wkt, WktValue) # noqa: S101
  512. for k, v in json.items():
  513. val = cast("Value", value_desc.type())
  514. _value_from_json(val, v, opts, value_wkt)
  515. msg.fields[k] = val
  516. def _list_value_from_json(
  517. msg: ListValue,
  518. json: JsonValue,
  519. opts: FromJsonOptions,
  520. values_desc: DescFieldValueList,
  521. ) -> None:
  522. if not isinstance(json, list):
  523. err = f"cannot decode {msg._desc.type_name} from JSON: {json}"
  524. raise TypeError(err)
  525. element_desc = values_desc.element
  526. assert isinstance(element_desc, DescMessage) # noqa: S101
  527. element_wkt = match_wkt(element_desc)
  528. assert isinstance(element_wkt, WktValue) # noqa: S101
  529. for e in json:
  530. val = cast("Value", element_desc.type())
  531. _value_from_json(val, e, opts, element_wkt)
  532. msg.values.append(val)
  533. def _value_from_json(
  534. msg: Value, json: JsonValue, opts: FromJsonOptions, wkt: WktValue
  535. ) -> None:
  536. match json:
  537. case None:
  538. msg.kind = Oneof(
  539. "null_value", cast("NullValue", wkt.null_value.enum.type(0))
  540. )
  541. case bool():
  542. msg.kind = Oneof("bool_value", json)
  543. case int() | float():
  544. msg.kind = Oneof("number_value", float(json))
  545. case str():
  546. msg.kind = Oneof("string_value", json)
  547. case list():
  548. lv_desc = wkt.list_value.message
  549. lv_wkt = match_wkt(lv_desc)
  550. assert isinstance(lv_wkt, WktListValue) # noqa: S101
  551. lv = cast("ListValue", lv_desc.type())
  552. _list_value_from_json(lv, json, opts, lv_wkt.values)
  553. msg.kind = Oneof("list_value", lv)
  554. case dict():
  555. struct_desc = wkt.struct_value.message
  556. struct_wkt = match_wkt(struct_desc)
  557. assert isinstance(struct_wkt, WktStruct) # noqa: S101
  558. struct = cast("Struct", struct_desc.type())
  559. _struct_from_json(struct, json, opts, struct_wkt.fields)
  560. msg.kind = Oneof("struct_value", struct)
  561. case _:
  562. assert_never(json)
  563. def _no_duplicates(pairs: list[tuple[str, JsonValue]]) -> dict[str, JsonValue]:
  564. """Reject duplicate JSON keys at parse time via json.loads object_pairs_hook.
  565. This is needed in addition to the seen_fields check in _read_message
  566. because a single proto field can have two distinct JSON keys (its proto
  567. name and its json_name). seen_fields catches that case, but it cannot
  568. catch two identical JSON keys that map to the same dict entry, since
  569. Python's default JSON parser silently keeps only the last value.
  570. """
  571. obj: dict[str, JsonValue] = {}
  572. for k, v in pairs:
  573. if k in obj:
  574. msg = f"duplicate key: {k}"
  575. raise ValueError(msg)
  576. obj[k] = v
  577. return obj
  578. def message_from_json_value(
  579. message_type: type[T],
  580. data: JsonValue,
  581. *,
  582. ignore_unknown_fields: bool = False,
  583. registry: Registry | None = None,
  584. ) -> T:
  585. """Converts the Python value parsed from JSON data to a new Message of the given type.
  586. This can be useful when embedding a message within a larger structure encoded as JSON.
  587. The JSON data will be read using ProtoJSON semantics, including support for
  588. well-known-types and extensions if a registry is provided.
  589. See [`message_to_json_value`][] for the inverse operation.
  590. Examples:
  591. ```python
  592. # For example, a request to a task runner
  593. data = json.loads(
  594. '{"task": "save_user", "user": {"name": "Alice", "created_at": "2024-01-01T00:00:00Z"}}'
  595. )
  596. user = message_from_json_value(User, data["user"])
  597. assert user.name == "Alice"
  598. assert user.created_at == Timestamp.from_datetime(
  599. datetime(2024, 1, 1, tzinfo=timezone.utc)
  600. )
  601. ```
  602. """
  603. message = message_type()
  604. opts = FromJsonOptions(
  605. ignore_unknown_fields=ignore_unknown_fields, registry=registry
  606. )
  607. _read_message(message, data, opts)
  608. return message