{"record":{"id":"a985c190664fa547","repo":"HumanSignal/label-studio","slug":"created-annotations-are-incompatible-with-provided","errorCode":null,"errorMessage":"Created annotations are incompatible with provided labeling schema, we found:\\n{diff_str}","messagePattern":"Created annotations are incompatible with provided labeling schema, we found:\\\\n(.+?)","errorType":"validation","errorClass":"ValidationError","httpStatus":400,"severity":"error","filePath":"label_studio/projects/models.py","lineNumber":717,"sourceCode":"            for ann_tuple in different_annotations:\n                from_name, to_name, t = ann_tuple.split('|')\n                # TODO tags that operate as both object and control tags; should be special registry/logic for them\n                if from_name == to_name and t.lower() == 'chatmessage':\n                    continue\n                if t.lower() == 'textarea':  # avoid textarea to_name check (see DEV-1598)\n                    continue\n                if (\n                    not check_control_in_config_by_regex(config_string, from_name)\n                    or not check_toname_in_config_by_regex(config_string, to_name)\n                    or t not in get_all_types(config_string)\n                ):\n                    diff_str.append(\n                        f'{self.summary.created_annotations[ann_tuple]} '\n                        f'with from_name={from_name}, to_name={to_name}, type={t}'\n                    )\n            if len(diff_str) > 0:\n                diff_str = '\\n'.join(diff_str)\n                raise ValidationError(\n                    f'Created annotations are incompatible with provided labeling schema, we found:\\n{diff_str}'\n                )\n\n        # validate labels consistency\n        labels_from_config, dynamic_label_from_config = get_all_labels(config_string)\n        created_labels = merge_labels_counters(self.summary.created_labels, self.summary.created_labels_drafts)\n\n        def display_count(count: int, type: str) -> Optional[str]:\n            \"\"\"Helper for displaying pluralized sources of validation errors,\n            eg \"1 draft\" or \"3 annotations\"\n            \"\"\"\n            if not count:\n                return None\n            return f'{count} {type}{\"s\" if count > 1 else \"\"}'\n\n        parsed_config = parse_config(config_string)\n        tag_types = {tag_info['type'] for _, tag_info in parsed_config.items()}\n","sourceCodeStart":699,"sourceCodeEnd":735,"githubUrl":"https://github.com/HumanSignal/label-studio/blob/0b49e9b53917880baf1dd85d574fe5541a9aafb2/label_studio/projects/models.py#L699-L735","documentation":"Label Studio validates that annotations already created on tasks are still compatible with a newly submitted labeling config. When the new config changes a control tag (name, to_name, or type) so that existing annotation results no longer match the schema, this ValidationError is raised listing the incompatible results via diff_str. It prevents configs from silently invalidating existing annotations.","triggerScenarios":"Calling POST /api/projects/<id>/validate-label-config or saving a project config (Project.validate_config) after annotations exist while renaming a from_name, changing to_name, or changing the control type (e.g. 'choices' to 'textarea') for results already stored.","commonSituations":"Editing an XML label config to rename a <Choices name='label'> tag after annotators have already submitted results; switching a control type mid-project; importing a project config from a template without checking existing annotations.","solutions":["Keep the original control tag names and types in the new config so existing annotation results stay valid","Delete or export-and-reset the incompatible annotations before applying the new config","If labels were merely renamed intentionally, migrate existing annotation results to the new names first (update result JSON from_name values)","Add the old tag back alongside the new one (keep both tags in config) so old results remain schema-valid"],"exampleFix":"// before\n<View>\n  <Text name='transcription' value='text'/>\n  <Choices name='sentiment' toName='transcription'>...</Choices>\n</View>\n// after (keep 'sentiment' name/type; only change labels inside)\n<View>\n  <Text name='transcription' value='text'/>\n  <Choices name='sentiment' toName='transcription'>\n    <Choice value='Positive'/><Choice value='Negative'/><Choice value='Neutral'/>\n  </Choices>\n</View>","handlingStrategy":"validation","validationCode":"// Before saving config, diff control tags against existing annotations\nconst results = await api.get(`/api/projects/${projectId}/tasks`, {params:{page_size:1}});\nif (results.count > 0) {\n  // ensure all from_name/to_name/type combos used in annotations are unchanged\n  const annotations = await api.get('/api/annotations', {params:{project: projectId}});\n  const used = new Set(annotations.results.flatMap(a => a.result.map(r => `${r.from_name}|${r.to_name}|${r.type}`)));\n  // parse new config and verify each tag in `used` still exists with same name/type\n}","typeGuard":"function keepsExistingControlTags(oldConfig, newConfig, usedTags) {\n  return usedTags.every(t => newConfig.includes(`name=\"${t.fromName}\"`));\n}","tryCatchPattern":"try {\n  await api.post(`/api/projects/${projectId}/validate-label-config`, {label_config: newConfig});\n} catch (e) {\n  if (e.response?.status === 400 && /incompatible with provided labeling schema/.test(e.response.data?.config ?? '')) {\n    // surface diff to user; do not save until tags preserved or annotations reset\n  } else throw e;\n}","preventionTips":["Always call validate-label-config before persisting a config on projects with annotations","Never rename control tags once annotation has started; only edit label values inside","Freeze tag names/types in project conventions or config-as-code reviews","Export annotations before any config migration"],"tags":["label-studio","validation","labeling-config","annotations"],"backgroundTag":"schema-validation-failed","analyzedSha":"0b49e9b53917880baf1dd85d574fe5541a9aafb2","analyzedAt":"2026-08-29T00:39:52.578Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}