JSON Model Example#
Simple example to visualize the values of a JSON file.
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
import json
import sys
from typing import Any, List, Dict, Union
from PySide6.QtWidgets import QTreeView, QApplication, QHeaderView
from PySide6.QtCore import QAbstractItemModel, QModelIndex, QObject, Qt, QFileInfo
class TreeItem:
    """A Json item corresponding to a line in QTreeView"""
    def __init__(self, parent: "TreeItem" = None):
        self._parent = parent
        self._key = ""
        self._value = ""
        self._value_type = None
        self._children = []
    def appendChild(self, item: "TreeItem"):
        """Add item as a child"""
        self._children.append(item)
    def child(self, row: int) -> "TreeItem":
        """Return the child of the current item from the given row"""
        return self._children[row]
    def parent(self) -> "TreeItem":
        """Return the parent of the current item"""
        return self._parent
    def childCount(self) -> int:
        """Return the number of children of the current item"""
        return len(self._children)
    def row(self) -> int:
        """Return the row where the current item occupies in the parent"""
        return self._parent._children.index(self) if self._parent else 0
    @property
    def key(self) -> str:
        """Return the key name"""
        return self._key
    @key.setter
    def key(self, key: str):
        """Set key name of the current item"""
        self._key = key
    @property
    def value(self) -> str:
        """Return the value name of the current item"""
        return self._value
    @value.setter
    def value(self, value: str):
        """Set value name of the current item"""
        self._value = value
    @property
    def value_type(self):
        """Return the python type of the item's value."""
        return self._value_type
    @value_type.setter
    def value_type(self, value):
        """Set the python type of the item's value."""
        self._value_type = value
    @classmethod
    def load(
        cls, value: Union[List, Dict], parent: "TreeItem" = None, sort=True
    ) -> "TreeItem":
        """Create a 'root' TreeItem from a nested list or a nested dictonary
        Examples:
            with open("file.json") as file:
                data = json.dump(file)
                root = TreeItem.load(data)
        This method is a recursive function that calls itself.
        Returns:
            TreeItem: TreeItem
        """
        rootItem = TreeItem(parent)
        rootItem.key = "root"
        if isinstance(value, dict):
            items = sorted(value.items()) if sort else value.items()
            for key, value in items:
                child = cls.load(value, rootItem)
                child.key = key
                child.value_type = type(value)
                rootItem.appendChild(child)
        elif isinstance(value, list):
            for index, value in enumerate(value):
                child = cls.load(value, rootItem)
                child.key = index
                child.value_type = type(value)
                rootItem.appendChild(child)
        else:
            rootItem.value = value
            rootItem.value_type = type(value)
        return rootItem
class JsonModel(QAbstractItemModel):
    """ An editable model of Json data """
    def __init__(self, parent: QObject = None):
        super().__init__(parent)
        self._rootItem = TreeItem()
        self._headers = ("key", "value")
    def clear(self):
        """ Clear data from the model """
        self.load({})
    def load(self, document: dict):
        """Load model from a nested dictionary returned by json.loads()
        Arguments:
            document (dict): JSON-compatible dictionary
        """
        assert isinstance(
            document, (dict, list, tuple)
        ), "`document` must be of dict, list or tuple, " f"not {type(document)}"
        self.beginResetModel()
        self._rootItem = TreeItem.load(document)
        self._rootItem.value_type = type(document)
        self.endResetModel()
        return True
    def data(self, index: QModelIndex, role: Qt.ItemDataRole) -> Any:
        """Override from QAbstractItemModel
        Return data from a json item according index and role
        """
        if not index.isValid():
            return None
        item = index.internalPointer()
        if role == Qt.DisplayRole:
            if index.column() == 0:
                return item.key
            if index.column() == 1:
                return item.value
        elif role == Qt.EditRole:
            if index.column() == 1:
                return item.value
    def setData(self, index: QModelIndex, value: Any, role: Qt.ItemDataRole):
        """Override from QAbstractItemModel
        Set json item according index and role
        Args:
            index (QModelIndex)
            value (Any)
            role (Qt.ItemDataRole)
        """
        if role == Qt.EditRole:
            if index.column() == 1:
                item = index.internalPointer()
                item.value = str(value)
                self.dataChanged.emit(index, index, [Qt.EditRole])
                return True
        return False
    def headerData(
        self, section: int, orientation: Qt.Orientation, role: Qt.ItemDataRole
    ):
        """Override from QAbstractItemModel
        For the JsonModel, it returns only data for columns (orientation = Horizontal)
        """
        if role != Qt.DisplayRole:
            return None
        if orientation == Qt.Horizontal:
            return self._headers[section]
    def index(self, row: int, column: int, parent=QModelIndex()) -> QModelIndex:
        """Override from QAbstractItemModel
        Return index according row, column and parent
        """
        if not self.hasIndex(row, column, parent):
            return QModelIndex()
        if not parent.isValid():
            parentItem = self._rootItem
        else:
            parentItem = parent.internalPointer()
        childItem = parentItem.child(row)
        if childItem:
            return self.createIndex(row, column, childItem)
        else:
            return QModelIndex()
    def parent(self, index: QModelIndex) -> QModelIndex:
        """Override from QAbstractItemModel
        Return parent index of index
        """
        if not index.isValid():
            return QModelIndex()
        childItem = index.internalPointer()
        parentItem = childItem.parent()
        if parentItem == self._rootItem:
            return QModelIndex()
        return self.createIndex(parentItem.row(), 0, parentItem)
    def rowCount(self, parent=QModelIndex()):
        """Override from QAbstractItemModel
        Return row count from parent index
        """
        if parent.column() > 0:
            return 0
        if not parent.isValid():
            parentItem = self._rootItem
        else:
            parentItem = parent.internalPointer()
        return parentItem.childCount()
    def columnCount(self, parent=QModelIndex()):
        """Override from QAbstractItemModel
        Return column number. For the model, it always return 2 columns
        """
        return 2
    def flags(self, index: QModelIndex) -> Qt.ItemFlags:
        """Override from QAbstractItemModel
        Return flags of index
        """
        flags = super(JsonModel, self).flags(index)
        if index.column() == 1:
            return Qt.ItemIsEditable | flags
        else:
            return flags
    def to_json(self, item=None):
        if item is None:
            item = self._rootItem
        nchild = item.childCount()
        if item.value_type is dict:
            document = {}
            for i in range(nchild):
                ch = item.child(i)
                document[ch.key] = self.to_json(ch)
            return document
        elif item.value_type == list:
            document = []
            for i in range(nchild):
                ch = item.child(i)
                document.append(self.to_json(ch))
            return document
        else:
            return item.value
if __name__ == "__main__":
    app = QApplication(sys.argv)
    view = QTreeView()
    model = JsonModel()
    view.setModel(model)
    json_path = QFileInfo(__file__).absoluteDir().filePath("example.json")
    with open(json_path) as file:
        document = json.load(file)
        model.load(document)
    view.show()
    view.header().setSectionResizeMode(0, QHeaderView.Stretch)
    view.setAlternatingRowColors(True)
    view.resize(500, 300)
    app.exec()
{
    "id": "0001",
    "type": "donut",
    "name": "Cake",
    "ppu": 0.55,
    "batters":
        {
            "batter":
                [
                    { "id": "1001", "type": "Regular" },
                    { "id": "1002", "type": "Chocolate" },
                    { "id": "1003", "type": "Blueberry" },
                    { "id": "1004", "type": "Devil's Food" }
                ]
        },
    "topping":
        [
            { "id": "5001", "type": "None" },
            { "id": "5002", "type": "Glazed" },
            { "id": "5005", "type": "Sugar" },
            { "id": "5007", "type": "Powdered Sugar" },
            { "id": "5006", "type": "Chocolate with Sprinkles" },
            { "id": "5003", "type": "Chocolate" },
            { "id": "5004", "type": "Maple" }
        ]
}