I'm building a Python program with Flet as the UI that takes in a JSON file and displayes the information in the Flet UI, but also allows you to edit the JSON file and save it. When I run the program, anything that is in a nested array gives me Error: Unexpected Data Type. Below I have my code with some sample json. I'm pretty sure the offending line is in def build_json_editor -> if isinstance(value, (dict, list)): but I cannot figure it out. import flet as ft import json import os from typing import Optional, Dict, List, Any, Union class JSONEditorApp: def __init__(self): self.current_file: Optional[str] = None self.json_data = {} self.edited_data = {} self.is_data_loaded = False self.page = None def main(self, page: ft.Page): self.page = page page.title = "JSON Editor" page.theme_mode = ft.ThemeMode.LIGHT page.window_width = 900 page.window_height = 700 page.padding = 20 page.scroll = ft.ScrollMode.AUTO # File picker for selecting JSON files self.file_picker = ft.FilePicker(on_result=self.file_picker_result) page.overlay.append(self.file_picker) # Status bar self.status_text = ft.Text("No file loaded", color=ft.colors.GREY) # JSON display and editing area self.editor_container = ft.Container( content=ft.Text("Please load a JSON file to start editing", color=ft.colors.GREY_600), alignment=ft.alignment.center, expand=True, padding=20, border=ft.border.all(1, ft.colors.GREY_300), border_radius=5, ) # Sample JSON button for testing sample_button = ft.OutlinedButton( "Load Sample JSON", icon=ft.icons.CODE, on_click=self.load_sample_json ) # Main UI layout page.add( ft.Row( [ ft.ElevatedButton( "Open JSON File", icon=ft.icons.FILE_OPEN, on_click=lambda _: self.file_picker.pick_files( allowed_extensions=["json"] ), ), ft.ElevatedButton( "Save Changes", icon=ft.icons.SAVE, on_click=self.save_changes, disabled=not self.is_data_loaded, ), sample_button, ] ), self.status_text, ft.Divider(), self.editor_container, ) def load_sample_json(self, e): """Load a sample JSON with nested arrays for testing""" sample_json = { "name": "Project X", "version": 1.5, "active": True, "settings": { "debug": False, "timeout": 30 }, "team": [ { "id": 1, "name": "John Doe", "roles": ["developer", "tester"] }, { "id": 2, "name": "Jane Smith", "roles": ["manager", "designer"], "projects": [ { "name": "Alpha", "priority": 1, "tasks": ["design", "implementation", "testing"] }, { "name": "Beta", "priority": 2, "tasks": ["research"] } ] } ], "categories": ["web", "mobile", "desktop"], "metrics": [ [10, 20, 30], [40, 50, 60, [1, 2, 3]] ] } self.json_data = sample_json self.edited_data = self.json_data.copy() self.current_file = "sample.json" # Virtual file self.is_data_loaded = True # Enable save button for control in self.page.controls[0].controls: if isinstance(control, ft.ElevatedButton) and control.text == "Save Changes": control.disabled = False control.update() self.status_text.value = "Loaded: Sample JSON" self.status_text.update() self.refresh_editor() def file_picker_result(self, e: ft.FilePickerResultEvent): if not e.files or len(e.files) == 0: return try: file_path = e.files[0].path self.current_file = file_path with open(file_path, "r") as f: self.json_data = json.load(f) self.edited_data = json.loads(json.dumps(self.json_data)) # Deep copy self.status_text.value = f"Loaded: {os.path.basename(file_path)}" self.is_data_loaded = True # Enable save button for control in self.page.controls[0].controls: if isinstance(control, ft.ElevatedButton) and control.text == "Save Changes": control.disabled = False control.update() # Create editor from the loaded JSON data self.refresh_editor() self.status_text.update() except Exception as ex: self.status_text.value = f"Error loading file: {str(ex)}" self.status_text.update() def refresh_editor(self): editor_content = ft.Column(scroll=ft.ScrollMode.AUTO, expand=True, spacing=10) # Add JSON structure visualization json_view = self.build_json_editor(self.edited_data) editor_content.controls.append(json_view) # Update the editor container self.editor_container.content = editor_content self.editor_container.update() def build_json_editor(self, data: Any, path: str = "", level: int = 0) -> ft.Control: """Recursively build editor UI for JSON data with improved array handling""" # Container for this level of JSON container = ft.Column(tight=True, spacing=5) if isinstance(data, dict): # Create a card for dictionaries for key, value in data.items(): current_path = f"{path}.{key}" if path else key item_row = ft.Row([ ft.Text(f"{key}:", weight=ft.FontWeight.BOLD, width=150) ], vertical_alignment=ft.CrossAxisAlignment.START) if isinstance(value, (dict, list)): # Add collapsible section for nested objects nested_content = self.build_json_editor(value, current_path, level + 1) expander = ft.ExpansionTile( title=ft.Text( f"{key} ({self.get_type_description(value)})", weight=ft.FontWeight.BOLD ), subtitle=ft.Text(self.get_preview_text(value)), controls=[ ft.Container( content=nested_content, padding=ft.padding.only(left=20) ) ], #initially_expanded=level < 1 # Auto-expand first two levels ) container.controls.append(expander) else: # Add editor for primitive values value_editor = self.create_value_editor(value, current_path) item_row.controls.append(value_editor) container.controls.append(item_row) elif isinstance(data, list): # Create vertical layout for arrays array_container = ft.Column(tight=True, spacing=10) # Add controls for adding new items add_item_row = ft.Row([ ft.OutlinedButton( "Add Item", icon=ft.icons.ADD, on_click=lambda e, p=path: self.add_array_item(e, p) ) ]) array_container.controls.append(add_item_row) # Add each array item with its index for i, item in enumerate(data): current_path = f"{path}[{i}]" # Create a row for this array item item_container = ft.Container( content=ft.Column([ ft.Row([ ft.Text(f"Index {i}:", weight=ft.FontWeight.BOLD), ft.IconButton( icon=ft.icons.DELETE, tooltip="Delete item", on_click=lambda e, p=current_path: self.delete_array_item(e, p) ) ]), ft.Container( content=self.build_json_editor(item, current_path, level + 1), padding=ft.padding.only(left=20) ) ]), border=ft.border.all(1, ft.colors.GREY_300), border_radius=5, padding=10, margin=ft.margin.only(bottom=5) ) array_container.controls.append(item_container) container = array_container else: # This shouldn't be reached normally container.controls.append(ft.Text(f"Error: Unexpected data type")) return container def get_type_description(self, value: Any) -> str: """Get a descriptive text for the type of a value""" if isinstance(value, dict): return f"Object with {len(value)} properties" elif isinstance(value, list): return f"Array with {len(value)} items" else: return str(type(value).__name__) def get_preview_text(self, value: Any) -> str: """Get a preview text for complex values""" if isinstance(value, dict): keys = list(value.keys()) if len(keys) > 2: return f"{', '.join(keys[:2])}, ..." return ', '.join(keys) elif isinstance(value, list): if len(value) > 2: return f"{str(value[0])[:10]}, {str(value[1])[:10]}, ..." elif len(value) > 0: return ', '.join(str(x)[:10] for x in value) return "empty array" return str(value) def create_value_editor(self, value: Any, path: str) -> ft.Control: """Create an appropriate editor control based on the value type""" if isinstance(value, bool): # Dropdown for boolean values dropdown = ft.Dropdown( width=200, options=[ ft.dropdown.Option("true"), ft.dropdown.Option("false") ], value="true" if value else "false", on_change=lambda e, p=path: self.update_value(e, p) ) return dropdown elif isinstance(value, (int, float)): # Number input for numeric values return ft.TextField( value=str(value), width=200, keyboard_type=ft.KeyboardType.NUMBER, on_change=lambda e, p=path: self.update_value(e, p) ) else: # Text input for strings and other types return ft.TextField( value=str(value), width=400, multiline=len(str(value)) > 50, min_lines=1, max_lines=5, on_change=lambda e, p=path: self.update_value(e, p) ) def update_value(self, e, path: str): """Update a value in the JSON data structure by path""" try: # Parse the path parts = self.parse_path(path) # Navigate to the parent object parent, last_key = self.navigate_to_parent(parts) # Get original value to determine type original_value = parent[last_key] # Convert value based on original type if isinstance(original_value, bool): new_value = e.control.value.lower() == "true" elif isinstance(original_value, int): new_value = int(e.control.value) elif isinstance(original_value, float): new_value = float(e.control.value) else: new_value = e.control.value # Update the value parent[last_key] = new_value # Show success status self.status_text.value = f"Updated: {path}" self.status_text.update() except Exception as ex: self.status_text.value = f"Error updating value: {str(ex)}" self.status_text.update() def add_array_item(self, e, path: str): """Add a new item to an array in the JSON data""" try: # Navigate to the array parts = self.parse_path(path) current = self.edited_data # Find the target array for part in parts: if isinstance(part, str): current = current[part] else: # It's an index current = current[part] # Determine what type of item to add based on existing items if len(current) > 0: # Use same type as last item last_item = current[-1] if isinstance(last_item, dict): new_item = {} elif isinstance(last_item, list): new_item = [] elif isinstance(last_item, str): new_item = "" elif isinstance(last_item, bool): new_item = False elif isinstance(last_item, (int, float)): new_item = 0 else: new_item = None else: # Default to empty string if array is empty new_item = "" # Add the new item current.append(new_item) # Refresh the editor self.refresh_editor() # Show success status self.status_text.value = f"Added item to array at {path}" self.status_text.update() except Exception as ex: self.status_text.value = f"Error adding item: {str(ex)}" self.status_text.update() def delete_array_item(self, e, path: str): """Delete an item from an array in the JSON data""" try: # Parse the path to get parent array and index parts = self.parse_path(path) # The last part should be the index to remove if not parts: return index = parts.pop() # Remove and get the last part (index) # Navigate to the parent array parent = self.edited_data for part in parts: if isinstance(part, str): parent = parent[part] else: # It's an index parent = parent[part] # Remove the item if isinstance(index, int) and 0 <= index < len(parent): parent.pop(index) # Refresh the editor self.refresh_editor() # Show success status self.status_text.value = f"Deleted item at index {index}" self.status_text.update() except Exception as ex: self.status_text.value = f"Error deleting item: {str(ex)}" self.status_text.update() def parse_path(self, path: str) -> List[Union[str, int]]: """Parse a path string into a list of keys and indices""" if not path: return [] parts = [] current = "" i = 0 while i < len(path): if path == '.': if current: parts.append(current) current = "" elif path == '[': if current: parts.append(current) current = "" # Find the matching closing bracket j = i + 1 while j < len(path) and path[j] != ']': j += 1 if j < len(path): # Extract the index index_str = path[i + 1:j] try: index = int(index_str) parts.append(index) except ValueError: pass i = j # Skip to after the closing bracket else: current += path i += 1 if current: parts.append(current) return parts def navigate_to_parent(self, parts: List[Union[str, int]]): """Navigate to the parent object of a path and return (parent, last_key)""" if not parts: return self.edited_data, None last_key = parts[-1] parent_path = parts[:-1] parent = self.edited_data for part in parent_path: if isinstance(part, str): parent = parent[part] else: # It's an index parent = parent[part] return parent, last_key def save_changes(self, e): if not self.current_file or not self.is_data_loaded: self.status_text.value = "No file to save" self.status_text.update() return try: # For sample.json, just show a message if self.current_file == "sample.json": self.status_text.value = "Changes would be saved to sample.json (virtual file)" self.status_text.update() return # Write the edited data to the file with open(self.current_file, "w") as f: json.dump(self.edited_data, f, indent=2) self.status_text.value = f"Saved changes to {os.path.basename(self.current_file)}" self.status_text.update() except Exception as ex: self.status_text.value = f"Error saving file: {str(ex)}" self.status_text.update() if __name__ == "__main__": app = JSONEditorApp() ft.app(target=app.main) Continue reading...