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" }
]
}