Reputation: 1642
I want to parse (json.loads) a json string that contains datetime values sent from a http client.
I know that I can write a custom json encoder by extending the default encoder and overriding the default method
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime,)):
return obj.isoformat()
elif isinstance(obj, (decimal.Decimal,)):
return str(obj)
else:
return json.JSONEncoder.default(self, obj)
My questions are -
Thanks,
Upvotes: 4
Views: 7912
Reputation: 69
The answer from Mause is very nice and complete in the sense you cannot make a wrong conversion due to the type specifier given. But typically JSON files aren't structured like this... More often you will find key-value or key-list pairs. However, using the object_hook approach still works for this, but you need to have keys only binding to one single type. For example, a key "start" will always contain a datetime value.
import json
import datetime
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime,)):
return obj.isoformat()
else:
return super().default(obj)
def cnv2DateTime(obj):
if isinstance(obj, list):
return [datetime.datetime.fromisoformat(i) for i in obj]
elif isinstance(obj, str):
return datetime.datetime.fromisoformat(obj)
else:
return obj
dtentries = ['range', 'start', 'end']
def object_hook(obj):
outDict = {}
for k, v in obj.items():
if k in dtentries:
vn = cnv2DateTime(v)
outDict[k] = vn
else:
outDict[k] = v
return outDict
def main():
data = {
"version": "1.0.0",
"range": [datetime.datetime.now(), datetime.datetime.now() + + datetime.timedelta(0, 60)],
"files": ['1', '2', '3'],
"entries": {'1': {'start': datetime.datetime(2022, 1, 1, 8, 0, 0), 'end': datetime.datetime(2022, 1, 12, 17, 45, 0)},
'2': {'start': datetime.datetime(2022, 2, 1, 8, 0, 0), 'end': datetime.datetime(2022, 2, 12, 17, 45, 0)}}
}
jdata = json.dumps(data, cls=MyJSONEncoder)
ddata = json.loads(jdata, object_hook=object_hook)
print('Done')
if __name__ == '__main__':
main()
As you can see from the example this works fine also for nested constructs or lists of datetime values. Hope, this is useful for somebody.
Upvotes: 0
Reputation: 638
There are likely other solutions, but json.load
& json.loads
both take an object_hook
argument1 that is called with every parsed object, with its return value being used in place of the provided object in the end result.
Combining this with a little tag in the object, something like this is possible;
import json
import datetime
import dateutil.parser
import decimal
CONVERTERS = {
'datetime': dateutil.parser.parse,
'decimal': decimal.Decimal,
}
class MyJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (datetime.datetime,)):
return {"val": obj.isoformat(), "_spec_type": "datetime"}
elif isinstance(obj, (decimal.Decimal,)):
return {"val": str(obj), "_spec_type": "decimal"}
else:
return super().default(obj)
def object_hook(obj):
_spec_type = obj.get('_spec_type')
if not _spec_type:
return obj
if _spec_type in CONVERTERS:
return CONVERTERS[_spec_type](obj['val'])
else:
raise Exception('Unknown {}'.format(_spec_type))
def main():
data = {
"hello": "world",
"thing": datetime.datetime.now(),
"other": decimal.Decimal(0)
}
thing = json.dumps(data, cls=MyJSONEncoder)
print(json.loads(thing, object_hook=object_hook))
if __name__ == '__main__':
main()
Upvotes: 8
Reputation: 1
As for 2nd question, you should propably just use
import dateutil.parser
dateutil.parser.parse('Your string')
method, it will try to parse your date string, and if it wouldn't be able to recognize it, it will throw Value Error. You can also use regular expressions to find fields that at least look like a dates (depending on the format you use, of course)
Upvotes: 0