safe_json_cast 1.1.1
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.
1.1.1 #
- The SDK floor drops to Dart 2.15 — Flutter 2.8 — from 3.12, widening who can
depend on this without changing anything about what it does. That is as low
as the code allows: 2.15 is where
EnumandEnum.namearrived, andasEnumis built on them. - The three constructs that had been holding the floor at Dart 3 were rewritten
in terms that predate it — a record in the path walker, a switch expression
in
asDuration, an unnamedlibrarydirective. None was load-bearing, and no behaviour changed with them: the same 120 tests pass.
1.1.0 #
Additive throughout — every 1.0.0 call site compiles unchanged, and the exception messages are byte-for-byte what they were.
JsonCastException, thrown by every cast. It extendsFormatException, so existingon FormatExceptionhandlers and message assertions keep working, and it exposes the same information as data —field,valueandexpectedType— for code that reports a bad payload rather than printing it.- Paths.
castAtreads a nested leaf in one step and reports the whole path as the field name:json.castAt('data.orders[0].price', asDouble)fails asField "data.orders[0].price"="n/a".valueAtreturns the raw value,hasPathanswers for a path whathasKeyanswers for a key, and both are available as the barevalueAtPath/pathExistsfor a non-map root. An absent step resolves tonull; a step that is present but not walkable — indexing into a string — throws, naming the part of the path that worked. - Bounds.
asDouble,asIntandasNumtakeminandmax, so a negative quantity or a percentage above 100 fails at the parse site rather than wherever it is first used. Inclusive, checked after conversion. asNum, for a field that is honestly either anintor adoubleand should not be rounded through one of them.asBigInt, for ids and balances that overflow a Dartinton the web. A fractional value is rejected rather than truncated.asUri, so a malformed URL fails at the parse site instead of at the request that would have used it.asStrictString, the counterpart toasStringfor fields where a number arriving in place of a string is a bug rather than something to coerce, andasNonEmptyString, because an API that sends""for an id is claiming to have sent one.asDurationwithDurationUnit— seconds by default, since that is whatexpiresInandttlusually mean. No unit guessing: a duration carries no magnitude that would give its unit away, so the caller states it.asEnum, matching an enum by name case-insensitively, with awireNamesmap for the spellings that differ —PARTIALLY_FILLEDtopartiallyFilled. The failure lists the accepted names.asMapOf<T>, for the objects an API uses as a dictionary — a balance per asset, a label per locale. Names the failing entry by key:"balances.BTC".asSet<T>and theasStringSetshorthand, for the arrays an API uses as a set. Order preserved; a repeat collapses by default, or throws underallowDuplicates: false, naming the entry that repeated.asIntListandasDoubleList, alongside the existing list shorthands.EpochUnitonasDateTime, to read a numeric timestamp in a known unit — including microseconds — rather than the magnitude guess, which stays the default.growableonasList, for the callers that were copying the result.tryCast, which turns a failed cast intonullso a deliberate default stays visible at the call site:tryCast(() => json.asDouble('fee')) ?? 0.- 120 tests, up from 58.
1.0.0 #
Initial release, extracted from an internal application module where these casts back roughly seventy request and response models.
- Strict and nullable casts for the JSON primitives —
asString,asCleanUrl,asDouble,asInt,asBoolandasDateTime— each taking the field's name and throwing aFormatExceptionthat quotes both the name and the offending value. - Lenient input by design: numeric strings with grouping commas,
0/1andyes/nobooleans, and epoch timestamps in seconds or milliseconds, since these are what financial APIs actually send. asMapandasList<T>for nested structures, replacing uncheckedas Map<String, dynamic>andas List<dynamic>casts.asListnames the failing entry by index —"trades[3]"— and its element callback receives that indexed name, so a whole path can be built up.asStringListandasMapListshorthands.- A
SafeJsonMapextension onMap<String, dynamic>mirroring every cast, so a field is named once (json.asDouble('lastPrice')) rather than twice. hasKey, to tell an absent field from one present and null — the casts treat the two alike.- Dartdoc across the public API, a runnable
example/, and 58 tests.