پیام «ورودی نامعتبر است» برای اصلاح یک فایل مهندسی کافی نیست. بهتر است روشن شود مشکل از خواندن JSON است، از واحد می‌آید یا از مقدار یک فیلد؛ بدون آنکه متن خام ورودی دوباره در پاسخ قرار گیرد. این درس یک قرارداد کوچک برای دادهٔ ساختگی حسگر و پاسخ خطای فارسی می‌سازد. کد کامل و هجده گروه بررسی اجراشده داریم؛ سرور، شبکه، حسگر واقعی و مدل هوش مصنوعی اجرا نمی‌شوند.

۱. JSON خوانده‌شده هنوز رکورد پذیرفته‌شده نیست

جدا بودن خواندن JSON از پذیرش قرارداد حسگر
تصویر مفهومی: جدا بودن خواندن JSON از پذیرش قرارداد حسگر؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

قرارداد نمونه دقیقاً چهار عضو می‌خواهد: sensor_id، sequence، voltage و unit. شناسه از الگوی لاتینِ اعلام‌شده پیروی می‌کند، شمارهٔ توالی صحیح مثبت و واحد دقیقاً V است. مقدار ولتاژ باید عددی در بازهٔ بستهٔ منفی پنج تا مثبت پنج باشد. این‌ها فرض‌های مثال‌اند؛ JSON به‌تنهایی چنین طرح داده‌ای برای حسگر تعریف نمی‌کند.

در RFC 8259، مقدار ریشه می‌تواند شکل‌های متفاوتی داشته باشد؛ یک آرایه، رشته یا null ممکن است JSON معتبر باشد. تابع ما فقط شیء ریشه را می‌پذیرد. رد کردن آرایه در اینجا نقض قرارداد حسگر است، نه ادعای نامعتبر بودن همهٔ آرایه‌ها در JSON. همچنین عضو اضافه نادیده گرفته و عضو کم با صفر پر نمی‌شود.

مستندات JSON پایتون رفتار خواننده و امکان سفارشی‌سازی آن را مشخص می‌کند. کد ابتدا متن را می‌خواند و سپس قواعد کاربرد را به ترتیب اجرا می‌کند. ساختار پاسخ نیز انتخاب ماست: موفقیت با ok و reading، خطا با code، path و message_fa. این تابع پیاده‌سازی سرور HTTP، OpenAPI یا یک استاندارد عمومی پاسخ خطا نیست.

۲. کلید تکراری را پیش از از دست رفتن اطلاعات ببینیم

تشخیص کلیدهای تکراری پیش از تبدیل به نگاشت
تصویر مفهومی: تشخیص کلیدهای تکراری پیش از تبدیل به نگاشت؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

دو مقدار برای نام voltage می‌تواند تفسیر پیام را مبهم کند. خوانندهٔ پیش‌فرض پایتون مقدار آخر را نگه می‌دارد؛ پس بررسی دیکشنری نهایی، همیشه راهی برای دیدن نام تکراری باقی نمی‌گذارد. در سیاست این درس، حتی دو مقدار برابر با یک نام نیز پذیرفته نمی‌شوند. توصیهٔ یکتایی نام‌ها در RFC را با الزام ساختگیِ همهٔ خواننده‌ها به رد کردن تکرار یکی نمی‌گیریم.

تابع object_pairs_hook جفت‌های نام و مقدارِ رمزگشایی‌شده را پیش از ساخت نگاشت نهایی به ما می‌دهد. اگر نامی قبلاً دیده شده باشد، duplicate_key برمی‌گردانیم. نامی که یک حرفش با escape نوشته شده، بعد از رمزگشایی می‌تواند با نام عادی برابر باشد؛ این مورد واقعاً آزموده شده است. تکرار داخل یک شیء تو‌در‌تو نیز پیش از بررسی طرح مسطحِ حسگر رد می‌شود.

برای این خطا path برابر $ است؛ hook سادهٔ ما مسیر کامل والدها را نگه نمی‌دارد. نسبت دادن یک نشانی دقیقِ ساختگی، عیب‌یابی را بهتر نمی‌کند. در خطاهای شناخته‌شدهٔ طرح، مسیر ثابت مانند $.unit داریم. این‌ها نشانه‌های سادهٔ محل فیلد هستند و مفسر عمومی یک زبان مسیر را پیاده نمی‌کنند.

۳. NaN، سرریز، نوع و واحد را جدا کنترل کنیم

کنترل مقدار متناهی، بازه و واحد داده
تصویر مفهومی: کنترل مقدار متناهی، بازه و واحد داده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

parse_constant برای توکن‌های NaN، Infinity و منفی Infinity خطا می‌سازد. اما عبارت نمایی 1e400 از نظر دستور زبان عدد JSON متفاوت است؛ تبدیل پیش‌فرض آن در این اجرا می‌تواند بی‌نهایت بدهد. بنابراین parse_float جدا نوشته‌ایم که پس از تبدیل، متناهی بودن را کنترل می‌کند. strict پیش‌فرض، به‌تنهایی این سیاست و منع نام تکراری را اجرا نمی‌کند.

پس از خواندن، شماره باید دقیقاً int باشد؛ true و ۷٫۰ پذیرفته نیستند. ولتاژ می‌تواند int یا float باشد، اما bool و رشتهٔ عددی رد می‌شوند. واحد mV نیز با حدس اندازهٔ عدد به V تبدیل نمی‌شود. ابتدا شناسه، سپس توالی، واحد و ولتاژ بررسی می‌شوند؛ تابع در این قرارداد فقط اولین خطای کشف‌شده را گزارش می‌کند و همهٔ خطاها را یکجا جمع نمی‌کند.

محدودیت نمایش عدد باقی می‌ماند: در آزمون 1e-400، تبدیل شناور صفرِ متناهی می‌دهد و این صفر از قرارداد می‌گذرد. راهنمای ممیز شناور پایتون توضیح می‌دهد چرا نمایش و گردشدن باید جدا دیده شوند. کنترل بازه روی مقدارِ نمایش‌داده‌شده است؛ دقت اعشاری نامحدود یا حفظ همهٔ فاصله‌های نزدیک مرز را تضمین نمی‌کند. اگر اعشار ثابت لازم است، درس قرارداد CSV نمونهٔ تبدیل متن به مقدار صحیح مقیاس‌دار را نشان می‌دهد.

۴. کد کامل و پاسخ دستیِ موفقیت و خطا

آزمایش پاسخ خطای پایدار با دادهٔ ساختگی
تصویر مفهومی: آزمایش پاسخ خطای پایدار با دادهٔ ساختگی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل sensor_json_contract.py را ذخیره و با پایتون اجرا کنید. اجرای ثبت‌شده Python 3.14.5 و فقط کتابخانهٔ استاندارد است. پیام ساختگی، شناسهٔ S01، توالی هفت، ولتاژ ۱٫۲۵ و واحد V دارد. پاسخ دستی باید همین شناسه و شماره را همراه voltage_v برابر ۱٫۲۵ حفظ کند. این مقدارِ خاص در نمایش دودویی دقیق است؛ چنین ادعایی برای همهٔ اعشارها نداریم.

"""Original decoded sensor JSON contract and controlled error response; no server."""
import json
import math
import platform
import re

MAX_CHARS = 4096
MESSAGES = {
    'input_text': 'متن ورودی باید رشته و حداکثر ۴۰۹۶ نویسه باشد.',
    'duplicate_key': 'نام هر عضو شیء را فقط یک بار وارد کنید.',
    'nonstandard_number': 'NaN و Infinity در ورودی مجاز نیستند.',
    'number_limit': 'عدد باید در نمایش عددی این برنامه متناهی باشد.',
    'syntax': 'ساختار JSON را بررسی کنید؛ یک مقدار کامل لازم است.',
    'structure_limit': 'ساختار ورودی از ظرفیت خوانندهٔ این اجرا فراتر رفت.',
    'object_required': 'ریشهٔ ورودی باید یک شیء JSON باشد.',
    'fields': 'فقط چهار فیلد sensor_id، sequence، voltage و unit لازم است.',
    'sensor_id': 'شناسه باید با حرف لاتین آغاز و حداکثر ۱۶ نویسهٔ مجاز داشته باشد.',
    'sequence': 'شماره باید عدد صحیح از یک تا ۲۱۴۷۴۸۳۶۴۷ باشد.',
    'unit': 'واحد باید دقیقاً V باشد.',
    'voltage_type': 'ولتاژ باید عدد JSON باشد؛ بولی یا رشته پذیرفته نیست.',
    'voltage_range': 'ولتاژ باید در بازهٔ بستهٔ منفی پنج تا مثبت پنج ولت باشد.',
}


class ContractError(ValueError):
    def __init__(self, code, path='$'):
        self.code, self.path = code, path


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ContractError('duplicate_key')
        result[key] = value
    return result


def reject_constant(token):
    raise ContractError('nonstandard_number')


def finite_json_float(token):
    value = float(token)
    if not math.isfinite(value):
        raise ContractError('number_limit')
    return value


def failed(code, path='$'):
    return {'ok': False, 'error': {'code': code, 'path': path, 'message_fa': MESSAGES[code]}}


def accept_sensor_json(text):
    try:
        if type(text) is not str or len(text) > MAX_CHARS:
            raise ContractError('input_text')
        payload = json.loads(text, object_pairs_hook=unique_object,
                             parse_constant=reject_constant, parse_float=finite_json_float)
        if type(payload) is not dict:
            raise ContractError('object_required')
        if set(payload) != {'sensor_id', 'sequence', 'voltage', 'unit'}:
            raise ContractError('fields')
        sensor_id = payload['sensor_id']
        if type(sensor_id) is not str or re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,15}', sensor_id) is None:
            raise ContractError('sensor_id', '$.sensor_id')
        sequence = payload['sequence']
        if type(sequence) is not int or not 1 <= sequence <= 2_147_483_647:
            raise ContractError('sequence', '$.sequence')
        if payload['unit'] != 'V':
            raise ContractError('unit', '$.unit')
        voltage = payload['voltage']
        if type(voltage) not in (int, float):
            raise ContractError('voltage_type', '$.voltage')
        if not -5 <= voltage <= 5:
            raise ContractError('voltage_range', '$.voltage')
        return {'ok': True, 'reading': {'sensor_id': sensor_id, 'sequence': sequence,
                                        'voltage_v': float(voltage)}}
    except ContractError as error:
        return failed(error.code, error.path)
    except json.JSONDecodeError:
        return failed('syntax')
    except RecursionError:
        return failed('structure_limit')
    except ValueError:
        # Decoder integer-string limits can depend on interpreter configuration.
        return failed('number_limit')


def run_example():
    fixture = '{"sensor_id":"S01","sequence":7,"voltage":1.25,"unit":"V"}'
    baseline = accept_sensor_json(fixture)
    expected = {'ok': True, 'reading': {'sensor_id': 'S01', 'sequence': 7, 'voltage_v': 1.25}}
    doc = lambda **changes: json.dumps({'sensor_id': 'S01', 'sequence': 7, 'voltage': 1.25, 'unit': 'V', **changes})
    error_is = lambda text, code, path='$': accept_sensor_json(text) == failed(code, path)
    overflow = fixture.replace('1.25', '1e400')
    underflow = accept_sensor_json(fixture.replace('1.25', '1e-400'))
    secret_marker = 'SYNTHETIC_RAW_PAYLOAD_NOT_FOR_RESPONSE'
    checks = {
        'valid_payload_matches_independent_hand_response': baseline == expected and json.loads(json.dumps(baseline, allow_nan=False)) == expected,
        'whitespace_order_and_finite_exponent_keep_declared_values': accept_sensor_json(' {"unit":"V", "voltage":1.25e0,"sequence":7,"sensor_id":"S01"} ') == expected,
        'duplicate_top_level_names_rejected_before_last_value_wins': error_is(fixture[:-1] + ',"voltage":2}', 'duplicate_key') and error_is(fixture[:-1] + ',"voltage":1.25}', 'duplicate_key'),
        'escaped_equivalent_names_are_duplicates_after_decoding': error_is(fixture[:-1] + r',"vol\u0074age":2}', 'duplicate_key'),
        'nested_duplicate_is_rejected_before_flat_schema_check': error_is(fixture[:-1] + ',"extra":{"x":1,"x":2}}', 'duplicate_key'),
        'nan_and_infinity_extensions_are_explicitly_rejected': all(error_is(fixture.replace('1.25', token), 'nonstandard_number') for token in ('NaN', 'Infinity', '-Infinity')),
        'valid_json_exponents_that_overflow_float_are_separately_rejected': error_is(overflow, 'number_limit') and error_is(fixture.replace('1.25', '-1e400'), 'number_limit'),
        'json_valid_nonobject_roots_violate_sensor_contract': all(error_is(text, 'object_required') for text in ('[]', 'null', '7', 'true', '"text"')),
        'missing_or_unknown_fields_are_not_filled_or_silently_ignored': error_is('{"sensor_id":"S01","sequence":7,"voltage":1.25}', 'fields') and error_is(fixture[:-1] + ',"extra":0}', 'fields'),
        'sequence_bool_float_zero_and_overflow_rejected_with_static_path': all(error_is(doc(sequence=value), 'sequence', '$.sequence') for value in (True, 7.0, 0, 2_147_483_648)),
        'unit_label_is_exact_and_not_inferred_from_magnitude': all(error_is(doc(unit=value), 'unit', '$.unit') for value in ('mV', 'v', '', None, 1)),
        'voltage_bool_string_null_or_container_rejected': all(error_is(doc(voltage=value), 'voltage_type', '$.voltage') for value in (True, '1.25', None, [], {})),
        'inclusive_voltage_bounds_and_smallest_authored_outside_probe': accept_sensor_json(doc(voltage=-5))['reading']['voltage_v'] == -5.0 and accept_sensor_json(doc(voltage=5))['reading']['voltage_v'] == 5.0 and all(error_is(doc(voltage=value), 'voltage_range', '$.voltage') for value in (-5.0001, 5.0001, 10**200)),
        'sensor_id_grammar_checked_without_echoing_bad_identifier': all(error_is(doc(sensor_id=value), 'sensor_id', '$.sensor_id') for value in ('1bad', 'نام', 'S' * 17, 'S\n1', 7)),
        'malformed_or_multiple_json_values_get_controlled_syntax_error': all(error_is(text, 'syntax') for text in ('', fixture + ' false', fixture[:-1] + ',}', '{sensor_id:1}')),
        'decoded_text_type_and_exact_character_budget_enforced': accept_sensor_json(' ' * (MAX_CHARS - len(fixture)) + fixture) == baseline and error_is(' ' * (MAX_CHARS + 1), 'input_text') and all(error_is(text, 'input_text') for text in (None, b'{}', 7)),
        'error_response_does_not_copy_input_keys_values_or_decoder_messages': secret_marker not in json.dumps(accept_sensor_json('{"' + secret_marker + '":"' + secret_marker + '"}'), ensure_ascii=False) and set(failed('fields')['error']) == {'code', 'path', 'message_fa'},
        'finite_float_policy_does_not_prevent_underflow_or_recover_exact_decimal': underflow == {'ok': True, 'reading': {'sensor_id': 'S01', 'sequence': 7, 'voltage_v': 0.0}},
    }
    assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values()), checks
    return {'python': platform.python_version(), 'dependencies': 'standard library only',
            'scope': 'Authored decoded flat JSON text contract; no network, hardware, calibration or schema standard implementation',
            'fixture_json': fixture, 'accepted_response': baseline,
            'duplicate_error': accept_sensor_json(fixture[:-1] + ',"voltage":2}'),
            'unit_error': accept_sensor_json(doc(unit='mV')),
            'overflow_error': accept_sensor_json(overflow),
            'underflow_probe': underflow, 'maximum_text_characters': MAX_CHARS,
            'checks': checks}


if __name__ == '__main__':
    print(json.dumps(run_example(), ensure_ascii=False, indent=2, allow_nan=False))

هجده بررسی نام‌دار، پاسخ دستی، تغییر ترتیب اعضا، نام تکراری و escape معادل، عدد نامجاز، سرریز، ریشهٔ نامناسب، نوع، واحد و مرز دامنه را می‌سنجند. دو انتهای مثبت و منفی پنج پذیرفته و مقدارهای ۵٫۰۰۰۱ و منفی ۵٫۰۰۰۱ رد می‌شوند. خروجی نمونه با allow_nan=False نیز سریال و دوباره خوانده شده است؛ اجرای تکراری تعداد آزمون‌های مستقل را زیاد نمی‌کند.

{
  "python": "3.14.5",
  "dependencies": "standard library only",
  "scope": "Authored decoded flat JSON text contract; no network, hardware, calibration or schema standard implementation",
  "fixture_json": "{\"sensor_id\":\"S01\",\"sequence\":7,\"voltage\":1.25,\"unit\":\"V\"}",
  "accepted_response": {
    "ok": true,
    "reading": {
      "sensor_id": "S01",
      "sequence": 7,
      "voltage_v": 1.25
    }
  },
  "duplicate_error": {
    "ok": false,
    "error": {
      "code": "duplicate_key",
      "path": "$",
      "message_fa": "نام هر عضو شیء را فقط یک بار وارد کنید."
    }
  },
  "unit_error": {
    "ok": false,
    "error": {
      "code": "unit",
      "path": "$.unit",
      "message_fa": "واحد باید دقیقاً V باشد."
    }
  },
  "overflow_error": {
    "ok": false,
    "error": {
      "code": "number_limit",
      "path": "$",
      "message_fa": "عدد باید در نمایش عددی این برنامه متناهی باشد."
    }
  },
  "underflow_probe": {
    "ok": true,
    "reading": {
      "sensor_id": "S01",
      "sequence": 7,
      "voltage_v": 0.0
    }
  },
  "maximum_text_characters": 4096,
  "checks": {
    "valid_payload_matches_independent_hand_response": true,
    "whitespace_order_and_finite_exponent_keep_declared_values": true,
    "duplicate_top_level_names_rejected_before_last_value_wins": true,
    "escaped_equivalent_names_are_duplicates_after_decoding": true,
    "nested_duplicate_is_rejected_before_flat_schema_check": true,
    "nan_and_infinity_extensions_are_explicitly_rejected": true,
    "valid_json_exponents_that_overflow_float_are_separately_rejected": true,
    "json_valid_nonobject_roots_violate_sensor_contract": true,
    "missing_or_unknown_fields_are_not_filled_or_silently_ignored": true,
    "sequence_bool_float_zero_and_overflow_rejected_with_static_path": true,
    "unit_label_is_exact_and_not_inferred_from_magnitude": true,
    "voltage_bool_string_null_or_container_rejected": true,
    "inclusive_voltage_bounds_and_smallest_authored_outside_probe": true,
    "sensor_id_grammar_checked_without_echoing_bad_identifier": true,
    "malformed_or_multiple_json_values_get_controlled_syntax_error": true,
    "decoded_text_type_and_exact_character_budget_enforced": true,
    "error_response_does_not_copy_input_keys_values_or_decoder_messages": true,
    "finite_float_policy_does_not_prevent_underflow_or_recover_exact_decimal": true
  }
}

ورودی باید str از قبل رمزگشایی‌شده و حداکثر ۴۰۹۶ نویسه باشد؛ این سقف بایت UTF-8 یا بودجهٔ حافظه نیست. محدودیت‌های خواننده دربارهٔ عدد صحیح و عمق می‌توانند به تنظیم مفسر وابسته باشند؛ کد برای خطاهای مربوط، پاسخ کنترل‌شده دارد. این مثال جای طراحی محدودیت کامل یک سرویس شبکه را نمی‌گیرد. پیام‌های خطا از جدول ثابت می‌آیند و نام اضافه، مقدار خام یا متن استثنای خواننده را کپی نمی‌کنند.

۵. چهار پرسش پاسخ‌دار برای طراحی تجربهٔ خطا

پیام خطای قابل اصلاح بدون بازتاب متن خام
تصویر مفهومی: پیام خطای قابل اصلاح بدون بازتاب متن خام؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: آیا strict=True برای منع NaN و تکرار کافی است؟ پاسخ: خیر؛ برای سیاست عدد و نام‌ها، hookهای صریح داریم. خطای ساختار، خطای قرارداد و محدودیت نمایش عدد نیز علت یکسانی ندارند.

پرسش دوم: چرا voltage برابر true عدد یک محسوب نمی‌شود؟ پاسخ: بولی در JSON نوع جداگانه‌ای است؛ کد نوع دقیق پایتونی را کنترل می‌کند تا معنای عددیِ حدسی وارد دادهٔ مهندسی نشود. همین سیاست برای توالی هم اجرا می‌شود.

پرسش سوم: آیا خطای واحد، ولتاژ را خودکار اصلاح می‌کند؟ پاسخ: خیر؛ پیام «واحد باید دقیقاً V باشد» فرصت اصلاح آگاهانه می‌دهد. تبدیل واقعی باید با قرارداد جدا و آزمون مقیاس انجام شود، نه فقط تعویض برچسب واحد.

پرسش چهارم: پاسخ بدون بازتاب متن خام، نگهداری داده را هم تعیین می‌کند؟ پاسخ: خیر؛ این تابع ثبت رویداد ندارد. گزارش ساختاریافتهٔ حسگر مرحلهٔ خروجی دیگری است و باید جدا طراحی شود. منابع در ۲۴ مهر ۱۴۰۵ خوانده شده‌اند؛ این تمرین آموزشی، خبر پژوهشی تازه، صحت فیزیکی حسگر یا امنیت کامل یک سامانه را اعلام نمی‌کند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.