Safeguard
Application Security•

Is deserialization safe in Dart and Flutter?

Flutter has no runtime reflection, which avoids class gadget attacks, but untrusted JSON still needs type checks, size limits and value validation.

Safeguard Security Team
5 min read

Safer than Java or Python, but not safe by default. Flutter has no runtime reflection, so JSON cannot instantiate arbitrary classes the way gadget chains do elsewhere. You still have to treat decoded data as untrusted: check types, set size limits, validate values, and avoid formats that run code.

What is the risk being avoided?

MITRE's CWE-502 describes the weakness as a product that deserializes untrusted data without sufficiently ensuring that the resulting data will be valid. The famous cases are in ecosystems where a stream can name a class to build, and the runtime obliges. That lets an attacker pick objects whose constructors or finalizers do something harmful.

Dart's mainstream path looks different. The Flutter documentation explains there is no Gson, Jackson or Moshi equivalent because such a library would need runtime reflection, which is disabled in Flutter, and reflection interferes with tree shaking. Instead you choose between manual decoding with jsonDecode() from dart:convert, or code generation with libraries such as json_serializable.

Both paths decode into plain maps and lists, and then your code builds the objects. The attacker cannot name a type. The remaining problems are quieter.

What still goes wrong?

Take a constructed example, a Flutter app that loads a user profile from an API and a "shared settings" file from a link the user taps:

final user = jsonDecode(body) as Map<String, dynamic>;
final age = user['age'] as int;
final site = Uri.parse(user['website'] as String);
launchUrl(site);

Four issues sit in these lines.

  • Type assumptions. The Flutter docs note that jsonDecode() returns dynamic, so types are unknown until runtime. A string where you expected an int throws, and an uncaught exception in a parsing path is a crash an attacker can trigger repeatedly.
  • Unvalidated values. The decoded value is attacker-influenced text. A website field may hold a scheme you never intended to open. Decoding succeeded, so the data is well formed, but it is not valid for your purpose. That distinction is the whole point of CWE-502's wording.
  • Size and depth. A very large or deeply nested document costs memory and time. The Dart isolates documentation shows how to run heavy work off the main isolate with Isolate.run(), which keeps the interface responsive, but it does not make an unbounded payload cheap. Cap the response size before you decode.
  • Trusting the source. A settings file received through a deep link or a shared file is untrusted input even if your own app wrote it last week.

How do I decode defensively?

Make decoding a single function that returns a typed result or a clear failure.

UserProfile? parseProfile(String body) {
  if (body.length > 64 * 1024) return null;
  try {
    final j = jsonDecode(body);
    if (j is! Map<String, dynamic>) return null;
    final age = j['age'];
    final site = j['website'];
    if (age is! int || age < 0 || age > 150) return null;
    if (site is! String) return null;
    final uri = Uri.tryParse(site);
    if (uri == null || !(uri.scheme == 'https')) return null;
    return UserProfile(age: age, website: uri);
  } on FormatException {
    return null;
  }
}

Generated code from json_serializable gives you typed fields and compile time checking of field names, which the Flutter docs list as a benefit. It does not decide whether an age of negative one is acceptable. Add range and format checks around the generated factory.

What about formats other than JSON?

Be careful with any format or package that can restore arbitrary object graphs or run code on load, including native platform channels that pass serialized blobs to Kotlin, Java or Swift code. Those platforms have their own deserialization history, and the safety of the Dart side does not carry over. Prefer plain data formats, and review any package that advertises polymorphic deserialization.

What to do

  1. Route all decoding through a few functions, never inline in widgets.
  2. Cap body size before calling jsonDecode().
  3. Check the runtime type of every field, and catch format errors.
  4. Validate ranges, lengths, and allowed URL schemes after decoding.
  5. Treat deep links, shared files and clipboard content as untrusted.
  6. Review plugins that cross into native code for serialization of their own.
  7. Scan pubspec.lock for vulnerable and abandoned packages.

Safeguard can scan Dart and Flutter dependency manifests and flag known vulnerable packages, which covers step 7.

Quick check

Does Flutter's lack of reflection make deserialization attacks impossible?

No. It removes the class-instantiation route. Crashes from bad types, unvalidated values, oversized payloads and risky native plugins remain.

Is json_serializable enough validation?

It gives typed fields and compile time field checks, but it does not know your business rules. Add range, format and scheme checks.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.