{"record":{"id":"d7ca55b2a4cc81c6","repo":"apache/druid","slug":"value-of-field-s-must-be-a-list-found-s","errorCode":null,"errorMessage":"Value of field %s must be a list, found %s","messagePattern":"Value of field (.+?) must be a list, found (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"server/src/main/java/org/apache/druid/catalog/model/ModelProperties.java","lineNumber":343,"sourceCode":"\n    @SuppressWarnings(\"unchecked\")\n    @Override\n    public Object merge(Object existing, Object updates)\n    {\n      if (updates == null) {\n        return existing;\n      }\n      if (existing == null) {\n        return updates;\n      }\n      List<T> existingList;\n      List<T> updatesList;\n      try {\n        existingList = (List<T>) existing;\n        updatesList = (List<T>) updates;\n      }\n      catch (ClassCastException e) {\n        throw new IAE(\n            \"Value of field %s must be a list, found %s\",\n            name,\n            updates.getClass().getSimpleName()\n        );\n      }\n      Set<T> existingSet = new HashSet<>(existingList);\n      List<T> revised = new ArrayList<>(existingList);\n      for (T col : updatesList) {\n        if (!existingSet.contains(col)) {\n          revised.add(col);\n        }\n      }\n      return revised;\n    }\n  }\n\n  class StringListPropertyDefn extends ListPropertyDefn<String>\n  {","sourceCodeStart":325,"sourceCodeEnd":361,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/server/src/main/java/org/apache/druid/catalog/model/ModelProperties.java#L325-L361","documentation":"A list-typed property in ModelProperties merges an existing list with an updates list (union via sets). This IAE is thrown when either the existing value or the update value is not actually a List — the merge operation requires both operands to be lists.","triggerScenarios":"Calling merge on a list-typed field where the stored value or the submitted update is a scalar, map, or string — e.g. updating a list property (like a dimensions list) with a single string instead of an array.","commonSituations":"Users submit \"dimensions\": \"col1\" instead of [\"col1\"]; a patch/update API call sends a map where the model stores a list; the stored model state was written by an older version using a different shape.","solutions":["Wrap the update value in a JSON array: pass [value] instead of value.","Check the existing stored value; if it is not a list, rewrite the whole property rather than merging.","If a custom ModelProperty definition declares list merging, verify its decode step actually produces a List."],"exampleFix":"// before\n{\"properties\": {\"dimensions\": \"col1\"}}\n// after\n{\"properties\": {\"dimensions\": [\"col1\"]}}","handlingStrategy":"validation","validationCode":"Object cur = existingProps.get(name);\nObject upd = updates.get(name);\nif (!(cur instanceof List) || !(upd instanceof List)) {\n  throw new IllegalArgumentException(name + \" requires list values for merge; wrap scalars in an array\");\n}","typeGuard":"boolean isListValue(Object v) { return v instanceof List; }","tryCatchPattern":"try { merged = propDef.merge(name, existing, updates); } catch (IllegalArgumentException e) { log.error(\"Merge requires lists for %s: %s\", name, e.getMessage()); }","preventionTips":["Always send array values for list-typed properties, even for single elements.","Never send a map or scalar where the stored property is a list.","When stored state may be non-list (legacy data), replace the property wholesale instead of merging."],"tags":["catalog","merge","list","type-mismatch"],"backgroundTag":"type-mismatch","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}