{"record":{"id":"7b8fafeaf576b80e","repo":"headroomlabs-ai/headroom","slug":"cls-r-must-be-a-dataclass","errorCode":null,"errorMessage":"{cls!r} must be a dataclass","messagePattern":"(.+?) must be a dataclass","errorType":"exception","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"headroom/testing/harness.py","lineNumber":405,"sourceCode":"            \"config_env_var\": self.config_env_var,\n        }\n\n    def validate(self) -> None:\n        \"\"\"Fail if the environment does not round-trip the full config payload.\"\"\"\n\n        raw = self.env.get(self.config_env_var)\n        if raw is None:\n            raise ValueError(f\"deployment env missing {self.config_env_var}\")\n        parsed = json.loads(raw)\n        if parsed != self.config_payload:\n            raise ValueError(f\"{self.config_env_var} does not match config_payload\")\n\n\ndef _field_contract(\n    owner: Literal[\"headroom\", \"proxy\"], cls: type[Any]\n) -> tuple[FieldContract, ...]:\n    if not is_dataclass(cls):\n        raise TypeError(f\"{cls!r} must be a dataclass\")\n    out: list[FieldContract] = []\n    for field in cls.__dataclass_fields__.values():\n        default: Any = MISSING\n        if field.default is not MISSING:\n            default = field.default\n        elif field.default_factory is not MISSING:  # type: ignore[attr-defined]\n            default = \"<factory>\"\n        out.append(\n            FieldContract(\n                owner=owner,\n                name=field.name,\n                type_repr=str(field.type),\n                has_default=default is not MISSING,\n                default_repr=None if default is MISSING else repr(default),\n            )\n        )\n    return tuple(out)\n","sourceCodeStart":387,"sourceCodeEnd":423,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/testing/harness.py#L387-L423","documentation":"TypeError raised by _field_contract() in headroom/testing/harness.py when it is called with a class that is not a Python dataclass (is_dataclass(cls) is False, line 407). The function walks cls.__dataclass_fields__ to build the (owner, name, type, default) FieldContract tuple used by the harness's config-schema checks, so only dataclasses are accepted — plain classes, NamedTuples, pydantic models, or TypedDicts all fail this check.","triggerScenarios":"Passing a plain class or a pydantic BaseModel to _field_contract(owner='headroom', cls=SomeConfig) instead of a @dataclass-decorated config class; refactoring a config dataclass into a pydantic model without updating the harness contract call.","commonSituations":"Migrating config classes to pydantic/attrs for validation while the testing harness still expects stdlib dataclasses; contributing new owner configs to the harness and forgetting the decorator.","solutions":["Decorate the class with @dataclass (or @dataclass(kw_only=True) as the neighboring configs use).","If the class is third-party (pydantic), wrap its fields in a small stdlib dataclass mirror for the contract check.","Confirm with dataclasses.is_dataclass(cls) in a unit test for every class passed to _field_contract."],"exampleFix":"# before\nclass ProxyCfg:  # plain class\n    mode: str = 'token'\n_field_contract('proxy', ProxyCfg)  # TypeError\n\n# after\nfrom dataclasses import dataclass\n\n@dataclass\nclass ProxyCfg:\n    mode: str = 'token'\n_field_contract('proxy', ProxyCfg)","handlingStrategy":"type-guard","validationCode":"from dataclasses import is_dataclass\n\nassert is_dataclass(cls), f'{cls!r} must be a dataclass'\ncontract = _field_contract('proxy', cls)","typeGuard":"from dataclasses import is_dataclass\nfrom typing import Type, Any\n\ndef is_dataclass_type(cls: Type[Any]) -> bool:\n    return is_dataclass(cls)","tryCatchPattern":null,"preventionTips":["Keep harness config classes as stdlib dataclasses.","Add a test asserting is_dataclass for every class registered with the harness."],"tags":["testing","dataclass","types"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}