[Python] Unexpected Data Type in Python Flet JSON Viewer and Editor

Discussão em 'Python' iniciado por Stack, Abril 18, 2025.

  1. Stack

    Stack Membro Participativo

    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...

Compartilhe esta Página