A Dart library for parsing partial and incomplete JSON strings, perfect for handling streaming JSON responses from Large Language Models (LLMs) and real-time APIs.
- ✅ Parse incomplete JSON - Handle JSON that's not fully received.
- ✅ Streaming support - Process JSON as it arrives, character by character, ensuring you get values as soon as possible
- ✅ Flexible string handling - Strict and non-strict modes for newline handling
- ✅ Zero dependencies - Only uses Dart's built-in libraries
- ✅ Type-safe - Full Dart null-safety support
- ✅ Well-tested - Comprehensive test coverage for edge cases
Add this to your package's pubspec.yaml file:
dependencies:
partial_json_stream_parser: ^1.0.0Then run:
dart pub getimport 'package:partial_json_stream_parser/partial_json_stream_parser.dart';
void main() {
final parser = PartialJsonParser();
// Parse incomplete JSON
final incomplete = '{"name": "John", "age": 30, "active": tr';
final result = parser.parse(incomplete);
print(result); // {name: John, age: 30, active: true}
}// Simulate receiving JSON in chunks (like from an LLM)
final chunks = [
'{"message": "Hello',
'{"message": "Hello, how',
'{"message": "Hello, how can I help',
'{"message": "Hello, how can I help you?"}'
];
final parser = PartialJsonParser();
for (final chunk in chunks) {
final result = parser.parse(chunk);
print('Current message: ${result['message']}');
}// Strict mode (default) - properly handles escape sequences
final strictParser = PartialJsonParser(strict: true);
final json1 = r'{"text": "Line 1\nLine 2"}'; // Escaped newline
print(strictParser.parse(json1)); // {text: Line 1\nLine 2}
// Non-strict mode - allows raw newlines in strings
final nonStrictParser = PartialJsonParser(strict: false);
final json2 = '{"text": "Line 1\nLine 2"}'; // Raw newline
print(nonStrictParser.parse(json2)); // {text: Line 1\nLine 2}final parser = PartialJsonParser();
// Incomplete object
parser.parse('{"key": "value"'); // {key: value}
// Missing values
parser.parse('{"key":'); // {key: null}
// Incomplete array
parser.parse('[1, 2, 3'); // [1, 2, 3]
// Trailing decimal
parser.parse('{"price": 19.'); // {price: 19}
// Incomplete boolean
parser.parse('{"active": t'); // {active: true}
// Incomplete null
parser.parse('{"data": n'); // {data: null}Handle cases where there's extra content after valid JSON:
final parser = PartialJsonParser(
onExtraToken: (text, data, remaining) {
print('Parsed: $data');
print('Extra content: $remaining');
},
);
parser.parse('{"valid": "json"} extra text');
// Parsed: {valid: json}
// Extra content: extra textPerfect for parsing responses from OpenAI, Anthropic, or other LLM APIs:
Stream<String> llmStream = getLLMResponseStream();
final parser = PartialJsonParser();
await for (final chunk in llmStream) {
final parsed = parser.parse(chunk);
// Update UI with partial response
updateUI(parsed['content']);
}Handle incomplete JSON frames from WebSocket connections:
webSocket.stream.listen((data) {
try {
final parsed = parser.parse(data);
processMessage(parsed);
} catch (e) {
// Handle completely malformed JSON
print('Invalid JSON received: $e');
}
});Render UI elements as soon as their data is available:
// As JSON streams in, render what's available
String buffer = '';
streamController.stream.listen((chunk) {
buffer += chunk;
final parsed = parser.parse(buffer);
// Render available fields immediately
if (parsed['title'] != null) showTitle(parsed['title']);
if (parsed['items'] != null) showItems(parsed['items']);
});The main parser class.
PartialJsonParser({
bool strict = true,
ExtraTokenCallback? onExtraToken,
})strict: Whether to use strict mode for string parsing (default: true)onExtraToken: Optional callback for handling extra tokens after valid JSON
dynamic parse(String input)Parses a potentially incomplete JSON string and returns the parsed value.
Result object containing the parsed value and remaining unparsed string.
class ParseResult {
final dynamic value;
final String remaining;
}The parser uses a recursive descent approach with the following strategy:
- Attempts standard JSON parsing first - If the input is valid JSON, it uses Dart's built-in parser
- Falls back to partial parsing - On failure, it identifies the JSON type and parses incrementally
- Handles incomplete tokens - Recognizes partial keywords (true, false, null) and incomplete strings
- Preserves parsed state - Returns the successfully parsed portion even if the input is incomplete
| Scenario | Standard jsonDecode |
PartialJsonParser |
|---|---|---|
| Complete valid JSON | ✅ Works | ✅ Works |
| Missing closing brackets | ❌ Throws | ✅ Returns partial |
| Incomplete strings | ❌ Throws | ✅ Returns partial |
| Trailing content | ❌ Throws | ✅ Handles with callback |
| Streaming data | ❌ Not supported | ✅ Designed for it |
- It doesn't recover or correct malformed or unordered JSON. You should ensure your JSON is passed with chars in the correct order to the parser, otherwise inconsistent results may be returned.
- It doesn't handle JSON with comments. You should ensure your JSON is passed without comments to the parser, otherwise an error will be thrown.
- It doesn't handle JSON with unquoted keys or unquoted string values. You should ensure your JSON is passed with quoted keys and string values to the parser, otherwise an error will be thrown.
Contributions are welcome! Feel free to submit a Pull Request.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
This Dart implementation is inspired by the Python partialjson library.