|
| 1 | +# dejajson |
| 2 | + |
| 3 | +Transparently decode DejaJson-encoded API responses with a drop-in |
| 4 | +[Dio](https://pub.dev/packages/dio) interceptor. |
| 5 | + |
| 6 | +Pairs with the [`nylo/dejajson`](https://github.com/nylo-core/laravel-deja-json) |
| 7 | +Laravel package: the server compresses JSON responses against a **dictionary |
| 8 | +trained on your own API** (zlib preset dictionaries — the compressor has |
| 9 | +effectively *already seen* each response before it starts), and |
| 10 | +`DejaJsonInterceptor` restores the original JSON before your app sees it. |
| 11 | +Your models, decoders and error handling don't change at all. |
| 12 | + |
| 13 | +Measured on realistic Laravel API payloads: |
| 14 | + |
| 15 | +| Payload | plain JSON | transport gzip | **dejajson** | |
| 16 | +|---|---|---|---| |
| 17 | +| single resource | 693 B | 437 B (−37%) | **58 B (−92%)** | |
| 18 | +| list of 25 | 17,469 B | 1,581 B (−91%) | **887 B (−95%)** | |
| 19 | +| list of 200 | 137,919 B | 8,002 B (−94%) | **7,303 B (−95%)** | |
| 20 | + |
| 21 | +- **Zero app-code changes** — `response.data` is plain JSON data, exactly as |
| 22 | + if the server had never encoded it. Error responses (422 validation errors |
| 23 | + and friends) are decoded too, and plain-JSON servers pass through exactly |
| 24 | + as Dio would have produced them. |
| 25 | +- **Safe by negotiation** — the interceptor advertises support (and its |
| 26 | + dictionary id) with an `X-Deja-Json` request header; servers only encode |
| 27 | + for clients that ask, and only use the dictionary when both sides hold |
| 28 | + identical bytes. A stale dictionary degrades to plain zlib, never to |
| 29 | + garbage. |
| 30 | +- **Native platforms** — zlib comes from `dart:io`. Web builds advertise |
| 31 | + nothing and receive plain JSON, so code stays portable. |
| 32 | + |
| 33 | +## Getting started |
| 34 | + |
| 35 | +```bash |
| 36 | +dart pub add dejajson # or: flutter pub add dejajson |
| 37 | +``` |
| 38 | + |
| 39 | +Train a dictionary server side and ship it as an asset: |
| 40 | + |
| 41 | +```bash |
| 42 | +# on the Laravel side |
| 43 | +php artisan dejajson:train storage/dejajson-samples |
| 44 | +cp storage/app/dejajson/dictionary.bin <your_app>/assets/dejajson/dictionary.bin |
| 45 | +``` |
| 46 | + |
| 47 | +```yaml |
| 48 | +# pubspec.yaml |
| 49 | +flutter: |
| 50 | + assets: |
| 51 | + - assets/dejajson/dictionary.bin |
| 52 | +``` |
| 53 | +
|
| 54 | +```dart |
| 55 | +import 'package:dio/dio.dart'; |
| 56 | +import 'package:dejajson/dejajson.dart'; |
| 57 | +import 'package:flutter/services.dart' show rootBundle; |
| 58 | + |
| 59 | +final bytes = await rootBundle.load('assets/dejajson/dictionary.bin'); |
| 60 | +final dictionary = DejaJsonDictionary.fromBytes(bytes.buffer.asUint8List()); |
| 61 | + |
| 62 | +final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com')) |
| 63 | + ..interceptors.add(DejaJsonInterceptor(dictionary: dictionary)); |
| 64 | + |
| 65 | +final response = await dio.get('/users'); |
| 66 | +print(response.data); // plain JSON — decoded transparently |
| 67 | +``` |
| 68 | + |
| 69 | +No dictionary yet? `DejaJsonInterceptor()` without one advertises mode `z` |
| 70 | +and still gets you ~gzip-level compression with no setup. |
| 71 | + |
| 72 | +After retraining on the server, ship the new file with your next app release. |
| 73 | +In between, old clients keep working: the ids no longer match, so the server |
| 74 | +simply falls back to mode `z` for them. |
| 75 | + |
| 76 | +## Using with Nylo |
| 77 | + |
| 78 | +Register the interceptor on your API service: |
| 79 | + |
| 80 | +```dart |
| 81 | +class ApiService extends NyApiService { |
| 82 | + ApiService({BuildContext? buildContext}) |
| 83 | + : super(buildContext, decoders: modelDecoders); |
| 84 | +
|
| 85 | + @override |
| 86 | + String get baseUrl => getEnv('API_BASE_URL'); |
| 87 | +
|
| 88 | + @override |
| 89 | + Map<Type, Interceptor> get interceptors => { |
| 90 | + ...super.interceptors, |
| 91 | + DejaJsonInterceptor: DejaJsonInterceptor(dictionary: dictionary), |
| 92 | + }; |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +Everything else — `network<User>()`, decoders, caching — keeps working |
| 97 | +unchanged, just with far smaller payloads on the wire. |
| 98 | + |
| 99 | +## Options |
| 100 | + |
| 101 | +```dart |
| 102 | +DejaJsonInterceptor( |
| 103 | + dictionary: dictionary, // the trained dictionary (null = mode "z" only) |
| 104 | + advertise: true, // add the request header (default true) |
| 105 | + modes: 'dz', // advertised modes; defaults to the platform's best |
| 106 | + rewriteContentType: true, // report application/json after decoding (default true) |
| 107 | + requestHeader: 'X-Deja-Json', // must match the server's dejajson.request_header |
| 108 | + codec: DejaJsonCodec(), // swap in a configured codec if needed |
| 109 | +) |
| 110 | +``` |
| 111 | + |
| 112 | +Notes: |
| 113 | + |
| 114 | +- Envelopes are binary, so for JSON requests the interceptor fetches bytes |
| 115 | + and rebuilds the response itself — plain-JSON and DejaJson servers both |
| 116 | + come out as ordinary decoded JSON. Requests with `ResponseType.bytes`, |
| 117 | + `stream` or `plain` are never advertised for and never touched. |
| 118 | +- A corrupt envelope (or one for a dictionary you don't hold) surfaces as a |
| 119 | + `DioException` (`badResponse`) whose `error` is a |
| 120 | + `DejaJsonFormatException`, rather than silently handing your app garbage. |
| 121 | +- If you call the API from browsers as well, remember to whitelist |
| 122 | + `X-Deja-Json` in the server's CORS `allowed_headers`. |
| 123 | + |
| 124 | +## Manual use |
| 125 | + |
| 126 | +The codec works without Dio — websockets, cached blobs, isolates: |
| 127 | + |
| 128 | +```dart |
| 129 | +const codec = DejaJsonCodec(); |
| 130 | +
|
| 131 | +final data = codec.decode(envelopeBytes, dictionary: dictionary); |
| 132 | +final envelope = codec.encode(data, dictionary: dictionary); // symmetric |
| 133 | +``` |
| 134 | + |
| 135 | +## Wire format (v1) |
| 136 | + |
| 137 | +``` |
| 138 | +byte 0–1 magic "DJ" |
| 139 | +byte 2 format version (0x01) |
| 140 | +byte 3 mode — "d" (zlib + shared dictionary) or "z" (zlib) |
| 141 | +byte 4+ zlib stream (RFC 1950) of the minified UTF-8 JSON document |
| 142 | +``` |
| 143 | + |
| 144 | +Sent with content type `application/vnd.dejajson`. zlib framing is |
| 145 | +deliberate: the stream embeds an Adler-32 of the dictionary (so a wrong |
| 146 | +dictionary fails loudly) and of the content (so corruption does too). |
| 147 | + |
| 148 | +One platform caveat inherited from JavaScript, not from DejaJson: on web |
| 149 | +builds, JSON numbers like `1.0` decode as the integer `1` — identical to how |
| 150 | +plain JSON behaves there. |
| 151 | + |
| 152 | +## Testing |
| 153 | + |
| 154 | +```bash |
| 155 | +dart test # VM suite |
| 156 | +dart test -p vm,chrome # additionally runs the web-platform tests in Chrome |
| 157 | +``` |
| 158 | + |
| 159 | +## License |
| 160 | + |
| 161 | +MIT © [Anthony Gordon](https://nylo.dev) |
0 commit comments