{"record":{"id":"14e08f8bb9c750cc","repo":"HumanSignal/label-studio","slug":"child-filters-cannot-contain-nested-child-filters","errorCode":null,"errorMessage":"Child filters cannot contain nested child filters","messagePattern":"Child filters cannot contain nested child filters","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"label_studio/data_manager/prepare_params.py","lineNumber":61,"sourceCode":"    @child_filter.setter\n    def child_filter(self, value: Optional['Filter']) -> None:\n        self.child_filters = [] if value is None else [value]\n\n\nclass ConjunctionEnum(Enum):\n    OR = 'or'\n    AND = 'and'\n\n\nclass Filters(BaseModel):\n    conjunction: ConjunctionEnum\n    items: List[Filter]\n\n    @model_validator(mode='after')\n    def validate_one_nesting_level(self):\n        for item in self.items:\n            if any(child.child_filters for child in item.child_filters):\n                raise ValueError('Child filters cannot contain nested child filters')\n        return self\n\n\nclass SelectedItems(BaseModel):\n    all: bool\n    included: List[int] = []\n    excluded: List[int] = []\n\n\nclass PrepareParams(BaseModel):\n    project: Union[int, List[int]]  # Support both single project and multiple projects\n    ordering: List[str] = []\n    selectedItems: Optional[SelectedItems] = None\n    filters: Optional[Filters] = None\n    data: Optional[dict] = None\n    request: Optional[Any] = None\n\n    @property","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/HumanSignal/label-studio/blob/0b49e9b53917880baf1dd85d574fe5541a9aafb2/label_studio/data_manager/prepare_params.py#L43-L79","documentation":"Pydantic model validator validate_one_nesting_level on the filter-group schema enforces that filters may be nested at most one level deep: if any filter's child_filters themselves contain child_filters, a ValueError('Child filters cannot contain nested child filters') is raised during request-body validation.","triggerScenarios":"Submitting a data manager view (POST/PATCH) whose filters JSON has a filter_group containing a filter with child_filters, and at least one of those child filters also has non-empty child_filters (grandchildren).","commonSituations":"Frontend builds a recursive/nested filter UI and sends the whole tree; a view JSON was hand-edited or migrated from a version allowing deeper nesting; filters from saved views with old schemas are replayed.","solutions":["Flatten the filter tree to at most one nesting level before submitting — move grandchild conditions into a sibling filter group.","Serialize conditions as multiple top-level filters combined by the group's conjunction.","Guard the filter-building code so appended child_filters are always leaf filters.","Wrap view submission in try/except ValueError (pydantic ValidationError) and show a nesting-depth message.","Inspect the saved view payload (e.g. via API GET) and manually fix the nested structure in the DB/export."],"exampleFix":"// before\n{\"items\": [{\"field\": \"f1\", \"child_filters\": [{\"field\": \"f2\", \"child_filters\": [{\"field\": \"f3\", \"child_filters\": []}]}]}]}\n// after\n{\"items\": [{\"field\": \"f1\", \"child_filters\": [{\"field\": \"f2\", \"child_filters\": []}]}, {\"field\": \"f3\", \"child_filters\": []}]}","handlingStrategy":"validation","validationCode":"def enforce_single_nesting(filters):\n    for item in filters.get('items', []):\n        for child in item.get('child_filters', []):\n            if child.get('child_filters'):\n                child['child_filters'] = []  # flatten or reject\n    return filters","typeGuard":"def is_one_level(filter_item: dict) -> bool:\n    return not any(c.get('child_filters') for c in filter_item.get('child_filters', []))","tryCatchPattern":"from pydantic import ValidationError\ntry:\n    params = PrepareParams.model_validate(request.data)\nexcept ValidationError as e:\n    if 'nested child filters' in str(e):\n        payload['filters'] = enforce_single_nesting(payload['filters'])\n        params = PrepareParams.model_validate(payload)\n    else:\n        raise","preventionTips":["Design filter UIs as exactly two levels (group + leaves)","Normalize/filter trees at the client boundary before sending","Add a serializer-level test for deep-nested payloads","When migrating old saved views, flatten them once at migration time"],"tags":["pydantic","validation","data-manager","filters"],"backgroundTag":"schema-validation-failed","analyzedSha":"0b49e9b53917880baf1dd85d574fe5541a9aafb2","analyzedAt":"2026-08-29T00:39:52.578Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}