{"record":{"id":"e33dafac5498eaf3","repo":"ZhuLinsen/daily_stock_analysis","slug":"parameters-must-be-an-object","errorCode":null,"errorMessage":"parameters must be an object","messagePattern":"parameters must be an object","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"src/services/alert_indicators.py","lineNumber":46,"sourceCode":"class TechnicalIndicatorAlert:\n    stock_code: str\n    alert_type: str\n    indicator_params: Dict[str, Any]\n    metadata: Dict[str, Any] = field(default_factory=dict)\n\n\n@dataclass\nclass IndicatorEvaluation:\n    status: str\n    observed_value: Optional[float]\n    threshold: Optional[float]\n    message: str\n    data_timestamp: Optional[datetime] = None\n\n\ndef normalize_indicator_parameters(alert_type: str, parameters: Dict[str, Any]) -> Dict[str, Any]:\n    if not isinstance(parameters, dict):\n        raise ValueError(\"parameters must be an object\")\n\n    if alert_type == \"ma_price_cross\":\n        normalized = {\n            \"direction\": _direction(parameters.get(\"direction\"), ABOVE_BELOW_DIRECTIONS, default=\"above\"),\n            \"window\": _int_in_range(parameters.get(\"window\"), \"window\", default=20),\n        }\n        return _ensure_required_bars_fetchable(alert_type, normalized)\n    if alert_type == \"rsi_threshold\":\n        normalized = {\n            \"direction\": _direction(parameters.get(\"direction\"), ABOVE_BELOW_DIRECTIONS, default=\"above\"),\n            \"period\": _int_in_range(parameters.get(\"period\"), \"period\", default=12),\n            \"threshold\": _float_in_range(parameters.get(\"threshold\"), \"threshold\", minimum=0.0, maximum=100.0),\n        }\n        return _ensure_required_bars_fetchable(alert_type, normalized)\n    if alert_type == \"macd_cross\":\n        fast_period = _int_in_range(parameters.get(\"fast_period\"), \"fast_period\", default=12)\n        slow_period = _int_in_range(parameters.get(\"slow_period\"), \"slow_period\", default=26)\n        if fast_period >= slow_period:","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/services/alert_indicators.py#L28-L64","documentation":"Raised by normalize_indicator_parameters (src/services/alert_indicators.py:46) when the parameters argument for a technical alert rule is not a Python dict / JSON object. The normalizer expects an object mapping field names (direction, window, period, ...) to values; arrays, strings, numbers, or null all fail this isinstance check before any field validation runs.","triggerScenarios":"Posting an alert rule (alert_type like ma_price_cross, rsi_threshold, macd_cross, kdj_cross, cci_threshold) with \"parameters\": [] or \"parameters\": \"window=20\" or \"parameters\": null in the JSON body; or a Python caller passing a list of tuples instead of a dict.","commonSituations":"Frontend form serializing parameters as a query string instead of an object; JSON schema drift where an older client sends an array; a migration script writing parameters as a JSON-encoded string rather than a parsed object.","solutions":["Send parameters as a JSON object: {\"parameters\": {\"direction\": \"above\", \"window\": 20}}.","If parameters arrive as a JSON string (double-encoded), parse it once with json.loads before calling the API/normalizer.","Add a schema check (JSON Schema / Pydantic model) at the API boundary so non-object payloads get a 422 instead of reaching this ValueError."],"exampleFix":"// before\n{ \"alert_type\": \"rsi_threshold\", \"parameters\": \"period=14,threshold=70\" }\n\n// after\n{ \"alert_type\": \"rsi_threshold\", \"parameters\": { \"period\": 14, \"threshold\": 70, \"direction\": \"above\" } }","handlingStrategy":"type-guard","validationCode":"if not isinstance(parameters, dict):\n    if isinstance(parameters, str):\n        import json; parameters = json.loads(parameters)  # unwrap double-encoded\n    else:\n        raise ValueError('parameters must be a JSON object')\nnormalized = normalize_indicator_parameters(alert_type, parameters)","typeGuard":"def is_indicator_parameters_object(v) -> bool:\n    return isinstance(v, dict) and all(isinstance(k, str) for k in v)","tryCatchPattern":"try:\n    normalize_indicator_parameters(alert_type, parameters)\nexcept ValueError as e:\n    if str(e) == 'parameters must be an object':\n        return bad_request('parameters must be a JSON object')\n    raise","preventionTips":["Type parameters as Dict[str, Any] (or a Pydantic model) at the API boundary.","Never JSON-encode parameters twice; serialize the outer payload once.","Contract-test the API with parameters: [] / 'x' / null expecting 4xx, not 500."],"tags":["validation","api-contract","alerts","json"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}