diff options
Diffstat (limited to 'python/google/protobuf/json_format.py')
-rw-r--r-- | python/google/protobuf/json_format.py | 214 |
1 files changed, 196 insertions, 18 deletions
diff --git a/python/google/protobuf/json_format.py b/python/google/protobuf/json_format.py index cb76e116..23382bdb 100644 --- a/python/google/protobuf/json_format.py +++ b/python/google/protobuf/json_format.py @@ -45,10 +45,11 @@ __author__ = 'jieluo@google.com (Jie Luo)' import base64 import json import math -from six import text_type +import six import sys from google.protobuf import descriptor +from google.protobuf import symbol_database _TIMESTAMPFOMAT = '%Y-%m-%dT%H:%M:%S' _INT_TYPES = frozenset([descriptor.FieldDescriptor.CPPTYPE_INT32, @@ -96,11 +97,15 @@ def MessageToJson(message, including_default_value_fields=False): def _MessageToJsonObject(message, including_default_value_fields): """Converts message to an object according to Proto3 JSON Specification.""" message_descriptor = message.DESCRIPTOR - if hasattr(message, 'ToJsonString'): - return message.ToJsonString() + full_name = message_descriptor.full_name if _IsWrapperMessage(message_descriptor): return _WrapperMessageToJsonObject(message) - return _RegularMessageToJsonObject(message, including_default_value_fields) + if full_name in _WKTJSONMETHODS: + return _WKTJSONMETHODS[full_name][0]( + message, including_default_value_fields) + js = {} + return _RegularMessageToJsonObject( + message, js, including_default_value_fields) def _IsMapEntry(field): @@ -109,9 +114,8 @@ def _IsMapEntry(field): field.message_type.GetOptions().map_entry) -def _RegularMessageToJsonObject(message, including_default_value_fields): +def _RegularMessageToJsonObject(message, js, including_default_value_fields): """Converts normal message according to Proto3 JSON Specification.""" - js = {} fields = message.ListFields() include_default = including_default_value_fields @@ -200,6 +204,79 @@ def _FieldToJsonObject( return value +def _AnyMessageToJsonObject(message, including_default): + """Converts Any message according to Proto3 JSON Specification.""" + if not message.ListFields(): + return {} + js = {} + type_url = message.type_url + js['@type'] = type_url + sub_message = _CreateMessageFromTypeUrl(type_url) + sub_message.ParseFromString(message.value) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + js['value'] = _WrapperMessageToJsonObject(sub_message) + return js + if full_name in _WKTJSONMETHODS: + js['value'] = _WKTJSONMETHODS[full_name][0](sub_message, including_default) + return js + return _RegularMessageToJsonObject(sub_message, js, including_default) + + +def _CreateMessageFromTypeUrl(type_url): + # TODO(jieluo): Should add a way that users can register the type resolver + # instead of the default one. + db = symbol_database.Default() + type_name = type_url.split('/')[-1] + try: + message_descriptor = db.pool.FindMessageTypeByName(type_name) + except KeyError: + raise TypeError( + 'Can not find message descriptor by type_url: {0}.'.format(type_url)) + message_class = db.GetPrototype(message_descriptor) + return message_class() + + +def _GenericMessageToJsonObject(message, unused_including_default): + """Converts message by ToJsonString according to Proto3 JSON Specification.""" + # Duration, Timestamp and FieldMask have ToJsonString method to do the + # convert. Users can also call the method directly. + return message.ToJsonString() + + +def _ValueMessageToJsonObject(message, unused_including_default=False): + """Converts Value message according to Proto3 JSON Specification.""" + which = message.WhichOneof('kind') + # If the Value message is not set treat as null_value when serialize + # to JSON. The parse back result will be different from original message. + if which is None or which == 'null_value': + return None + if which == 'list_value': + return _ListValueMessageToJsonObject(message.list_value) + if which == 'struct_value': + value = message.struct_value + else: + value = getattr(message, which) + oneof_descriptor = message.DESCRIPTOR.fields_by_name[which] + return _FieldToJsonObject(oneof_descriptor, value) + + +def _ListValueMessageToJsonObject(message, unused_including_default=False): + """Converts ListValue message according to Proto3 JSON Specification.""" + return [_ValueMessageToJsonObject(value) + for value in message.values] + + +def _StructMessageToJsonObject(message, unused_including_default=False): + """Converts Struct message according to Proto3 JSON Specification.""" + fields = message.fields + js = {} + for key in fields.keys(): + js[key] = _ValueMessageToJsonObject(fields[key]) + return js + + def _IsWrapperMessage(message_descriptor): return message_descriptor.file.name == 'google/protobuf/wrappers.proto' @@ -231,7 +308,7 @@ def Parse(text, message): Raises:: ParseError: On JSON parsing problems. """ - if not isinstance(text, text_type): text = text.decode('utf-8') + if not isinstance(text, six.text_type): text = text.decode('utf-8') try: if sys.version_info < (2, 7): # object_pair_hook is not supported before python2.7 @@ -240,7 +317,7 @@ def Parse(text, message): js = json.loads(text, object_pairs_hook=_DuplicateChecker) except ValueError as e: raise ParseError('Failed to load JSON: {0}.'.format(str(e))) - _ConvertFieldValuePair(js, message) + _ConvertMessage(js, message) return message @@ -291,13 +368,22 @@ def _ConvertFieldValuePair(js, message): if not isinstance(value, list): raise ParseError('repeated field {0} must be in [] which is ' '{1}.'.format(name, value)) - for item in value: - if item is None: - continue - if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + if field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: + # Repeated message field. + for item in value: sub_message = getattr(message, field.name).add() + # None is a null_value in Value. + if (item is None and + sub_message.DESCRIPTOR.full_name != 'google.protobuf.Value'): + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field.') _ConvertMessage(item, sub_message) - else: + else: + # Repeated scalar field. + for item in value: + if item is None: + raise ParseError('null is not allowed to be used as an element' + ' in a repeated field.') getattr(message, field.name).append( _ConvertScalarFieldValue(item, field)) elif field.cpp_type == descriptor.FieldDescriptor.CPPTYPE_MESSAGE: @@ -327,13 +413,87 @@ def _ConvertMessage(value, message): ParseError: In case of convert problems. """ message_descriptor = message.DESCRIPTOR - if hasattr(message, 'FromJsonString'): - message.FromJsonString(value) - elif _IsWrapperMessage(message_descriptor): + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): _ConvertWrapperMessage(value, message) + elif full_name in _WKTJSONMETHODS: + _WKTJSONMETHODS[full_name][1](value, message) else: _ConvertFieldValuePair(value, message) + +def _ConvertAnyMessage(value, message): + """Convert a JSON representation into Any message.""" + if isinstance(value, dict) and not value: + return + try: + type_url = value['@type'] + except KeyError: + raise ParseError('@type is missing when parsing any message.') + + sub_message = _CreateMessageFromTypeUrl(type_url) + message_descriptor = sub_message.DESCRIPTOR + full_name = message_descriptor.full_name + if _IsWrapperMessage(message_descriptor): + _ConvertWrapperMessage(value['value'], sub_message) + elif full_name in _WKTJSONMETHODS: + _WKTJSONMETHODS[full_name][1](value['value'], sub_message) + else: + del value['@type'] + _ConvertFieldValuePair(value, sub_message) + # Sets Any message + message.value = sub_message.SerializeToString() + message.type_url = type_url + + +def _ConvertGenericMessage(value, message): + """Convert a JSON representation into message with FromJsonString.""" + # Durantion, Timestamp, FieldMask have FromJsonString method to do the + # convert. Users can also call the method directly. + message.FromJsonString(value) + + +_INT_OR_FLOAT = six.integer_types + (float,) + + +def _ConvertValueMessage(value, message): + """Convert a JSON representation into Value message.""" + if isinstance(value, dict): + _ConvertStructMessage(value, message.struct_value) + elif isinstance(value, list): + _ConvertListValueMessage(value, message.list_value) + elif value is None: + message.null_value = 0 + elif isinstance(value, bool): + message.bool_value = value + elif isinstance(value, six.string_types): + message.string_value = value + elif isinstance(value, _INT_OR_FLOAT): + message.number_value = value + else: + raise ParseError('Unexpected type for Value message.') + + +def _ConvertListValueMessage(value, message): + """Convert a JSON representation into ListValue message.""" + if not isinstance(value, list): + raise ParseError( + 'ListValue must be in [] which is {0}.'.format(value)) + message.ClearField('values') + for item in value: + _ConvertValueMessage(item, message.values.add()) + + +def _ConvertStructMessage(value, message): + """Convert a JSON representation into Struct message.""" + if not isinstance(value, dict): + raise ParseError( + 'Struct must be in a dict which is {0}.'.format(value)) + for key in value: + _ConvertValueMessage(value[key], message.fields[key]) + return + + def _ConvertWrapperMessage(value, message): """Convert a JSON representation into Wrapper message.""" field = message.DESCRIPTOR.fields_by_name['value'] @@ -353,7 +513,8 @@ def _ConvertMapFieldValue(value, message, field): """ if not isinstance(value, dict): raise ParseError( - 'Map fieled {0} must be in {} which is {1}.'.format(field.name, value)) + 'Map field {0} must be in a dict which is {1}.'.format( + field.name, value)) key_field = field.message_type.fields_by_name['key'] value_field = field.message_type.fields_by_name['value'] for key in value: @@ -416,7 +577,7 @@ def _ConvertInteger(value): if isinstance(value, float): raise ParseError('Couldn\'t parse integer: {0}.'.format(value)) - if isinstance(value, text_type) and value.find(' ') != -1: + if isinstance(value, six.text_type) and value.find(' ') != -1: raise ParseError('Couldn\'t parse integer: "{0}".'.format(value)) return int(value) @@ -465,3 +626,20 @@ def _ConvertBool(value, require_str): if not isinstance(value, bool): raise ParseError('Expected true or false without quotes.') return value + +_WKTJSONMETHODS = { + 'google.protobuf.Any': [_AnyMessageToJsonObject, + _ConvertAnyMessage], + 'google.protobuf.Duration': [_GenericMessageToJsonObject, + _ConvertGenericMessage], + 'google.protobuf.FieldMask': [_GenericMessageToJsonObject, + _ConvertGenericMessage], + 'google.protobuf.ListValue': [_ListValueMessageToJsonObject, + _ConvertListValueMessage], + 'google.protobuf.Struct': [_StructMessageToJsonObject, + _ConvertStructMessage], + 'google.protobuf.Timestamp': [_GenericMessageToJsonObject, + _ConvertGenericMessage], + 'google.protobuf.Value': [_ValueMessageToJsonObject, + _ConvertValueMessage] +} |