{"record":{"id":"fb1be4422fc0970d","repo":"felixse/FluentTerminal","slug":"root-node-was-not-a-dictionary","errorCode":null,"errorMessage":"Root node was not a dictionary.","messagePattern":"Root node was not a dictionary\\.","errorType":"exception","errorClass":"ParseThemeException","httpStatus":null,"severity":"error","filePath":"FluentTerminal.App.Services/Implementation/ITermThemeParser.cs","lineNumber":67,"sourceCode":"        {\n            public const string BlueComponent = \"Blue Component\";\n            public const string GreenComponent = \"Green Component\";\n            public const string RedComponent = \"Red Component\";\n        }\n\n        public Task<TerminalTheme> Parse(string fileName, Stream fileContent)\n        {\n            if (string.IsNullOrWhiteSpace(fileName))\n            {\n                throw new ArgumentNullException(nameof(fileName));\n            }\n\n            if (fileContent == null)\n            {\n                throw new ArgumentNullException(nameof(fileContent));\n            }\n\n            var node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException(\"Root node was not a dictionary.\");\n\n            return Task.FromResult(new TerminalTheme\n            {\n                Name = Path.GetFileNameWithoutExtension(fileName),\n                Colors = GetColors(node),\n                Id = Guid.NewGuid(),\n                PreInstalled = false\n            });\n        }\n\n        private TerminalColors GetColors(DictionaryNode themeDictionary)\n        {\n            return new TerminalColors\n            {\n                Background = GetColorString(themeDictionary[ITermThemeKeys.BackgroundColor]),\n                Foreground = GetColorString(themeDictionary[ITermThemeKeys.ForegroundColor]),\n                Cursor = GetColorString(themeDictionary[ITermThemeKeys.CursorColor]),\n                CursorAccent = GetColorString(themeDictionary[ITermThemeKeys.CursorTextColor]),","sourceCodeStart":49,"sourceCodeEnd":85,"githubUrl":"https://github.com/felixse/FluentTerminal/blob/ba83ec485eea8d8ee65825711731300bee2e76ed/FluentTerminal.App.Services/Implementation/ITermThemeParser.cs#L49-L85","documentation":"Thrown by ITermThemeParser.Parse when PList.Load(fileContent) returns a node that is not a DictionaryNode. iTerm .itermcolors files are Apple property-list files whose root must be a dictionary keyed by color names (e.g. \"Ansi 0 Color\"). If the deserialized root is an array, string, data, or any non-dict PNode, the `as DictionaryNode` cast yields null and the ?? coalesce throws ParseThemeException.","triggerScenarios":"Calling Parse(fileName, stream) with a stream whose contents are not a plist dictionary: an empty file, a plist whose root is an <array>, a JSON plist that is a list, a binary plist with a non-dict top object, or a completely different file format passed with a .itermcolors extension.","commonSituations":"User downloads or hand-edits a .itermcolors theme and saves it as an array or strips the root dict; a theme exported by a different terminal (not iTerm) is renamed to .itermcolors; the file is truncated/corrupted during download so PListNet falls back to a non-dict node.","solutions":["Validate the file is a real iTerm theme before parsing: open it in a text editor (XML plists) and confirm the top-level <dict> ... </dict>; re-export from iTerm2 if missing.","Catch ParseThemeException around the Parse/Import call and show the user a friendly 'invalid theme file' message instead of crashing.","If authoring themes programmatically, ensure the serialized root is a DictionaryNode (PListNet.Nodes.DictionaryNode) before writing."],"exampleFix":"// before\nvar node = PList.Load(fileContent) as DictionaryNode ?? throw new ParseThemeException(\"Root node was not a dictionary.\");\n\n// after - surface the actual root type to aid debugging\nvar loaded = PList.Load(fileContent);\nvar node = loaded as DictionaryNode;\nif (node == null)\n    throw new ParseThemeException($\"Root node was not a dictionary (was {loaded?.GetType().Name ?? \"null\"}).\");","handlingStrategy":"validation","validationCode":"// Pre-validate the plist stream is a dict before handing off to the parser.\nusing (var reader = new StreamReader(fileContent)) { /* peek not trivial for binary plist */ }\n// Easiest: validate after load via PListNet and reject early:\nvar loaded = PList.Load(fileContent);\nif (!(loaded is PListNet.Nodes.DictionaryNode))\n    return Task.FromException<TerminalTheme>(\n        new ParseThemeException(\"Theme root must be a dictionary.\"));","typeGuard":"static bool IsDictionaryRoot(PNode node) => node is PListNet.Nodes.DictionaryNode;","tryCatchPattern":"try { theme = await parser.Parse(name, stream); }\ncatch (ParseThemeException ex) { logger.Warn(ex, \"Invalid theme {Name}\"); notifyUser(\"Theme file is not a valid iTerm theme.\"); }","preventionTips":["Only feed genuine iTerm2-exported .itermcolors files to ITermThemeParser.","Wrap Parse/Import in try/ParseThemeException at the UI layer and show a friendly message.","Validate SupportedFileTypes extension before parsing to reject obvious mismatches."],"tags":["theme-parsing","plist","iterm","input-validation"],"backgroundTag":null,"analyzedSha":"ba83ec485eea8d8ee65825711731300bee2e76ed","analyzedAt":"2026-08-13T21:23:15.467Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}