JSON Tree Viewer in Flutter: Deep Dive into voo_json_tree
Building developer tools in Flutter means you'll eventually need to display JSON data in a way that doesn't make users want to quit your app. Raw JSON dumps are brutal to read. Pretty printing helps, but navigating complex nested structures still feels like archaeology.
That's where voo_json_tree comes in. It's a Flutter package that turns JSON into an interactive tree with collapsible nodes, syntax highlighting, and editing capabilities. We built it at VooStack after wrestling with clunky alternatives in our AgileStack and DevStack products.
The package lives at pub.dev/packages/voo_json_tree with source code on GitHub.
The Real Problem This Solves
Say you're building an API testing tool. Users need to inspect response payloads that look like this:
{
"user": {
"id": 12345,
"profile": {
"name": "Jane Smith",
"preferences": {
"notifications": {
"email": true,
"push": false,
"categories": ["updates", "security", "billing"]
},
"privacy": {
"showProfile": true,
"allowMessages": false
}
}
},
"lastLogin": "2024-01-15T14:30:00Z",
"permissions": ["read", "write", "admin"]
},
"metadata": {
"version": "v2.1",
"cached": true
}
}
Displaying this in a Text widget is useless. Users can't collapse sections they don't care about. They can't quickly scan for specific keys. And if they need to edit values for testing, they're stuck.
The alternatives aren't great either. Most JSON tree packages for Flutter are either abandoned, have terrible APIs, or lack editing support. We evaluated flutter_json_widget, json_tree, and others before building our own.
Quick Start: Get It Running
Install the package:
flutter pub add voo_json_tree
Minimal working example:
import 'package:flutter/material.dart';
import 'package:voo_json_tree/voo_json_tree.dart';
class JsonTreeDemo extends StatelessWidget {
final Map<String, dynamic> data = {
"name": "Product API",
"version": "1.0.0",
"endpoints": [
{"path": "/users", "method": "GET"},
{"path": "/users/:id", "method": "PUT"}
]
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("JSON Tree Viewer")),
body: Padding(
padding: EdgeInsets.all(16.0),
child: VooJsonTree(
json: data,
onChanged: (updatedJson) {
print("JSON updated: $updatedJson");
},
),
),
);
}
}
This renders an interactive tree where users can expand/collapse nodes, edit values inline, and see syntax highlighting for different data types.
Core Concepts and Mental Model
The package treats JSON as a hierarchical structure of nodes. Each node can be:
- Primitive: strings, numbers, booleans, null
- Object: key-value pairs that can be expanded/collapsed
- Array: ordered lists that can be expanded/collapsed
The mental model is simple. Start with collapsed nodes showing just the data type and size ("Object (4)", "Array (12)"). Users click to expand what they care about. Primitive values are editable inline with validation.
Key Configuration Options
VooJsonTree(
json: myData,
expandAll: false, // Start with nodes collapsed
showTypes: true, // Display data types next to values
enableEditing: true, // Allow inline editing
theme: JsonTreeTheme(
keyStyle: TextStyle(color: Colors.blue),
stringStyle: TextStyle(color: Colors.green),
numberStyle: TextStyle(color: Colors.orange),
),
onChanged: (updatedJson) {
// Handle changes
},
)
Real-World Usage Scenarios
Scenario 1: API Response Inspector
You're building a REST client. Users need to quickly scan API responses and understand the data structure:
class ApiResponseView extends StatefulWidget {
final Map<String, dynamic> response;
final String endpoint;
const ApiResponseView({
required this.response,
required this.endpoint,
});
@override
State<ApiResponseView> createState() => _ApiResponseViewState();
}
class _ApiResponseViewState extends State<ApiResponseView> {
String searchQuery = '';
bool showOnlyMatches = false;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Search bar
Padding(
padding: EdgeInsets.all(8.0),
child: TextField(
decoration: InputDecoration(
hintText: "Search keys or values...",
prefixIcon: Icon(Icons.search),
),
onChanged: (query) {
setState(() => searchQuery = query);
},
),
),
// Response metadata
Container(
padding: EdgeInsets.all(12.0),
color: Colors.grey[100],
child: Text(
"${widget.endpoint} • ${_getResponseSize()} bytes",
style: TextStyle(fontWeight: FontWeight.w500),
),
),
// JSON tree
Expanded(
child: VooJsonTree(
json: widget.response,
searchQuery: searchQuery,
highlightMatches: true,
expandAll: false,
theme: JsonTreeTheme(
backgroundColor: Colors.white,
keyStyle: TextStyle(
color: Colors.blue[700],
fontWeight: FontWeight.w500,
),
stringStyle: TextStyle(color: Colors.green[700]),
numberStyle: TextStyle(color: Colors.orange[700]),
booleanStyle: TextStyle(color: Colors.purple[700]),
),
),
),
],
);
}
String _getResponseSize() {
// Rough JSON size calculation
final jsonString = json.encode(widget.response);
return (jsonString.length / 1024).toStringAsFixed(1) + 'KB';
}
}
Scenario 2: Configuration Editor
Users need to edit complex configuration files through a UI. Raw JSON editing is error-prone, but a tree view with validation works well:
class ConfigEditor extends StatefulWidget {
final Map<String, dynamic> initialConfig;
final Function(Map<String, dynamic>) onSave;
const ConfigEditor({
required this.initialConfig,
required this.onSave,
});
@override
State<ConfigEditor> createState() => _ConfigEditorState();
}
class _ConfigEditorState extends State<ConfigEditor> {
late Map<String, dynamic> currentConfig;
bool hasUnsavedChanges = false;
String? validationError;
@override
void initState() {
super.initState();
currentConfig = Map.from(widget.initialConfig);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Configuration Editor"),
actions: [
if (hasUnsavedChanges)
IconButton(
icon: Icon(Icons.save),
onPressed: _saveConfig,
),
],
),
body: Column(
children: [
if (validationError != null)
Container(
width: double.infinity,
padding: EdgeInsets.all(12.0),
color: Colors.red[50],
child: Text(
validationError!,
style: TextStyle(color: Colors.red[700]),
),
),
Expanded(
child: VooJsonTree(
json: currentConfig,
enableEditing: true,
expandAll: true, // Config files should be fully visible
onChanged: _handleConfigChange,
validators: {
'port': (value) => _validatePort(value),
'email': (value) => _validateEmail(value),
'timeout': (value) => _validatePositiveNumber(value),
},
),
),
],
),
);
}
void _handleConfigChange(Map<String, dynamic> updatedConfig) {
setState(() {
currentConfig = updatedConfig;
hasUnsavedChanges = true;
validationError = _validateConfig(updatedConfig);
});
}
void _saveConfig() {
if (validationError == null) {
widget.onSave(currentConfig);
setState(() => hasUnsavedChanges = false);
}
}
String? _validateConfig(Map<String, dynamic> config) {
// Custom validation logic
if (config['database']?['host'] == null) {
return 'Database host is required';
}
return null;
}
String? _validatePort(dynamic value) {
if (value is! int || value < 1 || value > 65535) {
return 'Port must be between 1 and 65535';
}
return null;
}
// Additional validators...
}
Performance Characteristics and Scale Limits
The package handles moderately large JSON structures well, but there are practical limits. We've tested it with:
- Sweet spot: JSON files up to 50KB with up to 1,000 nodes
- Acceptable: 100KB files with 2,000-3,000 nodes (some lag on older devices)
- Struggles: Anything over 200KB or 5,000+ nodes
The performance bottleneck is Flutter's widget tree depth. Each JSON node becomes a widget, and deeply nested structures hit platform limits. For huge JSON files, consider:
- Lazy loading: Only render visible nodes
- Pagination: Break large arrays into chunks
- Virtualization: Use a custom scroll view for massive lists
The package doesn't implement these optimizations yet, so you'll need to preprocess large datasets.
Memory Usage
Expect roughly 3-5x memory overhead compared to storing the raw JSON. The package keeps both the original data and the widget tree in memory. For a 50KB JSON file, budget around 200KB of additional RAM.
Common Gotchas and Integration Tips
Version pinning: Pin to a specific version in production. The package is at 0.1.1 and the API might change:
dependencies:
voo_json_tree: 0.1.1 # Pin exact version
Null safety: The package expects non-null JSON. Wrap nullable data:
VooJsonTree(
json: apiResponse ?? {}, // Provide fallback
)
Theme conflicts: The package's theme might clash with your app theme. Always provide a custom theme:
VooJsonTree(
theme: JsonTreeTheme(
backgroundColor: Theme.of(context).cardColor,
keyStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
)
Editing callbacks: The onChanged callback fires frequently during editing. Debounce it for expensive operations:
Timer? _debounceTimer;
void _handleJsonChange(Map<String, dynamic> json) {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 500), () {
// Expensive save operation
_saveToDatabase(json);
});
}
Where It Shines and Where It Doesn't
voo_json_tree excels at:
- Developer tools and admin interfaces
- Configuration editors with complex nested data
- API response inspection and debugging
- Educational apps that need to show JSON structure
It's not great for:
- Massive datasets (use virtualized alternatives)
- Simple key-value displays (overkill)
- Performance-critical UIs (too much widget overhead)
- Mobile apps with limited screen space (tree views are desktop-friendly)
The package fills a specific niche. If you need a JSON tree viewer in Flutter and don't want to build one from scratch, it's solid. But don't force it into use cases where a simple list or form would work better.
What This Means
voo_json_tree solves a real problem for Flutter developers building tools that work with JSON. It's not revolutionary, but it's well-executed and saves you from building your own tree widget.
The 0.1.1 version tag means expect some API changes. The package is young but functional. For production apps, test thoroughly and pin the version.
Start with the basic implementation, then layer on search, theming, and validation as needed. Don't try to handle massive datasets without preprocessing.
Grab the package from pub.dev and see if it fits your JSON display needs. Sometimes the simple solution is the right solution.
Want to dig deeper into voo_json_tree? Check out the package page for full docs and live stats. Need help integrating it into your stack? AgileStack helps teams adopt the right tools without the consulting-firm overhead. Book a 30-minute call.