safe_json_cast 1.1.1 copy "safe_json_cast: ^1.1.1" to clipboard
safe_json_cast: ^1.1.1 copied to clipboard

Typed casts for decoded JSON that fail loudly and name the field, so a malformed payload surfaces at the parse site instead of deep in the UI.

example/safe_json_cast_example.dart

// Parses two payloads with the same model: one well-formed, one with a broken
// field, to show what the failure reads like.
import 'dart:convert';

import 'package:safe_json_cast/safe_json_cast.dart';

/// The status an order arrives with. The wire spells two of these
/// differently, which [SafeJsonMap.asEnum] takes a map for.
enum OrderStatus { open, filled, partiallyFilled }

class Ticker {
  const Ticker({
    required this.symbol,
    required this.lastPrice,
    required this.priceChangePercent,
    required this.tradable,
    required this.updatedAt,
    required this.tags,
    this.note,
  });

  /// Reads a ticker off a decoded JSON object.
  ///
  /// Note `lastPrice`: the server sends it as a string to keep full precision,
  /// and [SafeJsonMap.asDouble] accepts that without a special case.
  factory Ticker.fromJson(Map<String, dynamic> json) => Ticker(
    symbol: json.asString('symbol'),
    lastPrice: json.asDouble('lastPrice'),
    priceChangePercent: json.asDouble('priceChangePercent'),
    tradable: json.asBool('tradable'),
    updatedAt: json.asDateTime('updateTime'),
    tags: json.asStringList('tags'),
    // Absent from the payload below; a nullable cast returns null rather
    // than throwing.
    note: json.asNullableString('note'),
  );

  final String symbol;
  final double lastPrice;
  final double priceChangePercent;
  final bool tradable;
  final DateTime updatedAt;
  final List<String> tags;
  final String? note;

  @override
  String toString() =>
      'Ticker($symbol, $lastPrice, $priceChangePercent%, '
      'tradable: $tradable, at: $updatedAt, tags: $tags, note: $note)';
}

void main() {
  const good = '''
  {
    "symbol": "BTCUSDT",
    "lastPrice": "64,120.55",
    "priceChangePercent": -1.42,
    "tradable": 1,
    "updateTime": 1766500000000,
    "tags": ["spot", "futures"]
  }
  ''';

  final ticker = Ticker.fromJson(jsonDecode(good) as Map<String, dynamic>);
  print(ticker);

  // The same payload with a price the server failed to fill in. The cast
  // throws where the field is read, naming it, instead of handing a NaN or a
  // zero to the rest of the app.
  const bad = '''
  {
    "symbol": "ETHUSDT",
    "lastPrice": "n/a",
    "priceChangePercent": 0.3,
    "tradable": true,
    "updateTime": "2026-08-23T09:15:00Z",
    "tags": []
  }
  ''';

  try {
    Ticker.fromJson(jsonDecode(bad) as Map<String, dynamic>);
  } on JsonCastException catch (error) {
    // FormatException: Field "lastPrice"="n/a" cannot be parsed as double.
    print(error);
    // The same failure as data, for a log line or an error report.
    print('field=${error.field} expected=${error.expectedType}');
  }

  // An element cast receives an indexed field name, so building the child
  // field name from it locates the bad entry rather than merely reporting it.
  const nested = '{"levels": [{"price": 1.0}, {"price": "oops"}]}';
  try {
    (jsonDecode(nested) as Map<String, dynamic>).asList(
      'levels',
      element: (Object? raw, String field) =>
          asDouble(asMap(raw, field: field)['price'], field: '$field.price'),
    );
  } on FormatException catch (error) {
    // FormatException: Field "levels[1].price"="oops" cannot be parsed as
    // double.
    print(error);
  }

  // Enums come off the wire in whatever case the API prefers, and sometimes
  // under a different name entirely.
  const order = '{"status": "PARTIALLY_FILLED", "fee": "n/a"}';
  final Map<String, dynamic> orderJson =
      jsonDecode(order) as Map<String, dynamic>;
  print(
    orderJson.asEnum(
      'status',
      values: OrderStatus.values,
      wireNames: const <String, OrderStatus>{
        'PARTIALLY_FILLED': OrderStatus.partiallyFilled,
      },
    ),
  );

  // A field that really is allowed a default: tryCast keeps the fallback at
  // the call site, where it can be read, rather than inside the cast.
  print(tryCast(() => orderJson.asDouble('fee')) ?? 0);

  // A leaf buried in an envelope: castAt walks to it and keeps the whole path
  // in the failure, instead of reporting the leaf's own name.
  const envelope = '''
  {
    "data": { "orders": [ { "price": "1.5" }, { "price": "n/a" } ] },
    "meta": { "expiresIn": 3600 }
  }
  ''';
  final Map<String, dynamic> envelopeJson =
      jsonDecode(envelope) as Map<String, dynamic>;
  print(envelopeJson.castAt('data.orders[0].price', asDouble));
  try {
    envelopeJson.castAt('data.orders[1].price', asDouble);
  } on JsonCastException catch (error) {
    // FormatException: Field "data.orders[1].price"="n/a" cannot be parsed as
    // double.
    print(error);
  }

  // A number an API means as seconds, which Duration should not have to be
  // reconstructed from by hand at every call site.
  print(envelopeJson.castAt('meta.expiresIn', asDuration));

  // A bound catches the value that converts cleanly but cannot be right.
  try {
    (<String, dynamic>{'sharePct': 150}).asDouble('sharePct', min: 0, max: 100);
  } on JsonCastException catch (error) {
    // FormatException: Field "sharePct"="150" is outside the range 0..100.
    print(error);
  }

  // An object the API uses as a dictionary: the keys are data, so they cannot
  // be spelled out in a model. A bad entry is named by key.
  const wallet = '{"balances": {"BTC": "0.5", "ETH": 2}}';
  print(
    (jsonDecode(wallet) as Map<String, dynamic>).asMapOf<double>(
      'balances',
      entry: (Object? raw, String field) => asDouble(raw, field: field),
    ),
  );
}
1
likes
160
points
162
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Typed casts for decoded JSON that fail loudly and name the field, so a malformed payload surfaces at the parse site instead of deep in the UI.

Homepage
Repository (GitHub)
View/report issues

Topics

#json #parsing #serialization #validation #casting

License

MIT (license)

More

Packages that depend on safe_json_cast