TATECHATLAS
◎ Русский
Программирование

Сериализация datetime и Decimal в JSON с явным контрактом на Python

Используйте строки ISO временных меток и десятичные строки, затем восстанавливайте поля явно. Полный пример стандартной библиотеки показывает проверки часового пояса, точность и предсказуемые сбои.

В этом материале

Используйте пользовательскую функцию json.dumps default для кодирования осведомленных дат-времени как ISO строк и конечных значений Decimal как десятичных строк. Восстановите именованные поля явно после json.loads. Это сохраняет представление образца временной метки смещения и Decimal(«12.30») без обработки произвольных строк как типизированных объектов. Пример проверяет два поля, а не полную бизнес-схему.

Определите контракт обмена

JSON не имеет выделенного типа datetime или Decimal. Определите, как каждый дополнительный тип Python будет пересекать границу, прежде чем выбирать кодировщик. Это руководство использует конкретный контракт: временная метка - это ISO строка, содержащая используемое смещение UTC, а сумма - конечная десятичная строка. Потребитель восстанавливает два поля явно. Ни строка, похожая на дату, ни строка, похожая на число, автоматически не является типизированным объектом Python.

Пример использует стандартную библиотеку Python и нацелен на Python 3.11 или новее. Его сконструированный вход - это UTC datetime на 29 сентября 2026 года в 12:00 и Decimal(«12.30»). Цель - сохранить это представление, а не сериализовать произвольные объекты или определить полную схему платежа.

Сериализация дат с учетом часового пояса

Дата-время осведомлена только тогда, когда tzinfo не None, и utcoffset() также возвращает значение. Кодировщик проверяет оба условия перед вызовом isoformat(). Отказ от наивных значений предотвращает бесшумное назначение локального часового пояса машины или путаницу с местным показанием часов с конкретным моментом.

Примерный временной штамп UTC становится 2026-09-29T12:00:00+00:00. Строка смещения идентифицирует смещение в этот момент, но не сохраняет имя зоны IANA или ее будущие правила летнего времени. Нормализуйте до UTC только тогда, когда это соответствует вашему контракту; этот пример не переинтерпретирует наивную временную метку как UTC.

Сохранение Decimal как строки

Decimal(«12.30») сохраняет свои десятичные цифры и конечный ноль. Преобразование его в двоичный float перед сериализацией изменяет представление и может вызвать округление. Вместо этого кодировщик возвращает str(obj), делая сумму строкой JSON. Получатель может восстановить его с Decimal без предварительного прохождения через float.

Пример отклоняет неконечные значения Decimal. Десятичная строка по-прежнему не указывает валюту, максимальную сумму или разрешенные десятичные цифры. Это отдельные бизнес-ограничения. JSON также поддерживает нативные числовые значения; выбор десятичной строки здесь - это явный контракт на межоперационную совместимость, а не заявление о том, что JSON не может содержать числа.

Построить явный кодировщик

Полный пример включает импорты, кодирование, восстановление и образцовый вход. json.dumps вызывает default только для объектов, которые его обычный кодировщик не может обработать. Пользовательская функция обрабатывает datetime и Decimal; другие неподдерживаемые пользовательские объекты вызывают TypeError. Обычные строки, целые числа, списки и словари остаются в ведении встроенного кодировщика.

allow_nan=False отклоняет нативный float NaN и Infinity во время сериализации. Проверка конечного Decimal отдельная, потому что Decimal обрабатывается пользовательской функцией. Третьесторонняя зависимость не требуется. Пример ниже иллюстративный; его ожидаемый вывод объясняется, а не представляется как выполненный тест.

import json
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation

def contract_encoder(obj):
    if isinstance(obj, datetime):
        if obj.tzinfo is None or obj.utcoffset() is None:
            raise TypeError("Timezone-aware datetime required")
        return obj.isoformat()
    if isinstance(obj, Decimal):
        if not obj.is_finite():
            raise TypeError("Finite Decimal required")
        return str(obj)
    raise TypeError("Unsupported custom value")

def reconstruct_data(data):
    if not isinstance(data, dict):
        raise ValueError("Object required")
    if not all(isinstance(data.get(key), str) for key in ("timestamp", "amount")):
        raise ValueError("timestamp and amount must be strings")
    timestamp = datetime.fromisoformat(data["timestamp"])
    if timestamp.tzinfo is None or timestamp.utcoffset() is None:
        raise ValueError("Timezone-aware timestamp required")
    amount = Decimal(data["amount"])
    if not amount.is_finite():
        raise ValueError("Finite amount required")
    return {"timestamp": timestamp, "amount": amount}

data = {"timestamp": datetime(2026, 9, 29, 12, 0, tzinfo=timezone.utc),
        "amount": Decimal("12.30")}
encoded = json.dumps(data, default=contract_encoder, allow_nan=False)
print(encoded)
restored = reconstruct_data(json.loads(encoded))
print(restored["timestamp"].isoformat(), str(restored["amount"]))

Прочитайте поля обратно

json.loads восстанавливает словари, списки и базовые значения. Он оставляет временную метку и сумму как строки. reconstruct_data проверяет, что верхнеуровневое значение - это объект, и оба обязательных поля - строки, затем вызывает datetime.fromisoformat и Decimal явно. Он также требует пригодной для использования временной метки смещения и конечной суммы.

Не преобразуйте каждую строку, которая похожа на дату или число. Значение поля исходит из согласованной схемы. Эти проверки охватывают два поля; они не отклоняют каждый дополнительный свойство или не обеспечивают соблюдение политик приложения размера, валюты, авторизации и диапазона. Приложения должны добавлять ограничения, которые их граница фактически требует.

Просмотрите иллюстративный вывод

Для сконструированного входа первая напечатанная строка - это {«timestamp»: «2026-09-29T12:00:00+00:00», «amount»: «12.30»}. Вторая - 2026-09-29T12:00:00+00:00 12.30. Это иллюстративные ожидаемые строки, выведенные из кода, а не отчет о выполненном тесте. Сумма находится в кавычках в JSON и все еще заканчивается на ноль после восстановления.

Сравните типизированные восстановленные значения с предполагаемым контрактом. Декодированная строка одна не доказывает, что восстановление произошло. Изменение контракта вывода на тегированный объект или числовую сумму JSON потребует соответствующих изменений в потребителях; не смешивайте форматы случайно.

Обработка недействительных значений предсказуемо

Кодирование наивной временной метки, неконечного Decimal или другого неподдерживаемого пользовательского объекта вызывает явную ошибку TypeError в примере. Неправильный JSON может вызвать json.JSONDecodeError. Неправильный текст ISO временной метки вызывает ValueError. Неправильный текст десятичной дроби обычно вызывает decimal.InvalidOperation в контексте по умолчанию, а не ValueError.

Проверки восстановления вызывают ValueError для неправильного верхнеуровневого типа, отсутствующих или нестроковых полей, временной метки без смещения и неконечной суммы. Ловушки Decimal настраиваемы, поэтому поведение исключения может зависеть от контекста. Обрабатывайте соответствующие сбои на границе приложения, не обещая универсальное описание трассировки стека или ведение журнала чувствительных полезных нагрузок.

Знайте, что JSON не может сохранить

JSON не помнит, был ли массив изначально кортежем или списком, или была ли строка изначально датой-временем или Decimal. Явное восстановление, поэтому, является частью этого контракта. Возврат строки из default достаточен здесь; пользовательские подклассы JSONEncoder и JSONDecoder являются необязательными альтернативами, а не обязательными требованиями.

object_hook вызывается для декодированных объектов JSON, а не для каждой примитивной строки. Он может поддерживать явно разработанный формат тегированных объектов, но это был бы другой контракт, чем два именованных строковых поля, используемые здесь. Сохранение представления полезно, но оно не устанавливает бизнес-действительность, именованные правила временных зон или безопасное потребление ресурсов.

Что проверить

  • Используйте Python 3.11 или новее для этого примера.
  • Проверяйте tzinfo и utcoffset() перед принятием datetime.
  • Держите сумму Decimal как строку JSON, а не преобразуйте ее в float.
  • Требуйте объекта со строковыми полями timestamp и amount во время восстановления.
  • Отклоняйте неконечные суммы и временные метки без смещения.
  • Различайте ValueError, json.JSONDecodeError и decimal.InvalidOperation на границе ввода.

Это сконструированная иллюстрация стандартной библиотеки, а не выполненный тест. Строка смещения не сохраняет именованные правила временных зон. Десятичные строки сохраняют представление, а не валюту или бизнес-действительность. Пример проверяет два поля и конечность; приложения все еще нуждаются в политиках диапазона, размера и авторизации.

Источники

  1. Python: JSON encoders and decoders ↗
  2. Python: datetime ISO serialization ↗
  3. Python: Decimal arithmetic ↗
Наверх ↑