Course outline

Dart / Integration: targets, resources, validation, and evidence / Lesson 17

Turn decoded JSON into validated local models

Separate decoding, structural checks, domain validation, explicit field selection, and safe rejection of untrusted data.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain why valid JSON and dynamic map access do not establish a trusted request.
  • Validate every required field and preserve a deliberate policy for normalization, unknown data, and partial acceptance.

Decoding establishes syntax, not the application contract

jsonDecode turns JSON text into Dart values such as maps, lists, strings, numbers, booleans, and null. A successfully decoded value can still have the wrong top-level shape or unacceptable field values. Treat the result as untrusted input rather than allowing dynamic access throughout domain code. Separate malformed JSON from structurally invalid data and business rejection when callers need different explanations. An identifier being a String does not prove it exists or belongs to the caller. A count being an int does not prove it falls within the application’s permitted range.

Construct a trusted object only after all required checks

The validator accepts an Object? boundary value and checks map shape and field types explicitly. It trims the identifier and allows counts from one to one thousand under an invented lab policy, not a universal service limit. A nullable result keeps the sample compact; a real API may need structured reasons for missing fields, wrong types, and out-of-range values. The class is trusted only for the checks shown. It does not authorize the identifier or establish persistence, and callers must not generalize its narrow validation into a complete security or booking guarantee.

dart Explicit shape, type, and range validation
import 'dart:convert';
class BatchRequest {
  final String id;
  final int count;
  BatchRequest(this.id, this.count);
}
BatchRequest? validateRequest(Object? value) {
  if (value is! Map<String, dynamic>) return null;
  final id = value['id'];
  final count = value['count'];
  if (id is! String || count is! int) return null;
  final normalized = id.trim();
  if (normalized.isEmpty || count < 1 || count > 1000) return null;
  return BatchRequest(normalized, count);
}
void main() {
  final Object? value = jsonDecode('{"id":" B-2 ","count":3}');
  final request = validateRequest(value);
  print(request == null ? 'invalid' : '${request.id}:${request.count}');
  print(validateRequest(<String, Object?>{'id': 'B-3', 'count': '3'}) == null);
}
Expected output
B-2:3
true

Expected output is B-2:3 then true. The string count is rejected rather than parsed implicitly. The source uses fixed JSON and local objects and was not executed.

Make normalization and unknown-field rules visible

Trimming can be appropriate for a human-entered lab code, but not automatically for every signed token or protocol identifier. Decide whether extra fields are ignored, rejected, or retained in a bounded extension area. Duplicate member-name policy may need handling before an ordinary decoded map has already collapsed the source representation. Likewise, a null field and a missing field may require separate treatment. These choices belong to the public data contract. Do not silently delete invalid list elements and then report complete success unless partial acceptance is the declared product behavior.

Encode an explicit output contract instead of an entire object

Select the fields that belong in an external representation rather than serializing every convenient internal property. The example creates a small public map and omits an internal note. This is field selection, not an authorization check or a complete privacy policy. Use jsonEncode to escape strings correctly instead of assembling JSON by concatenation. Numeric precision and supported values still depend on the data model and target; avoid assuming an arbitrarily large integer round-trips identically through every JavaScript consumer. Stable field meaning matters as much as syntactically valid output.

dart A deliberately selected JSON representation
import 'dart:convert';
void main() {
  final internal = <String, Object?>{
    'id': 'B-1',
    'count': 2,
    'internalNote': 'lab-only',
  };
  final publicValue = <String, Object?>{
    'id': internal['id'],
    'count': internal['count'],
  };
  print(jsonEncode(publicValue));
}
Expected output
{"id":"B-1","count":2}

Expected output is {"id":"B-1","count":2}. The internalNote field is omitted intentionally. No external data or service is accessed, and no encoding test was executed.

Bound input before trusting a convenient decoder

A decoder does not automatically impose the product’s input-size, nesting, list-length, or processing limits. Enforce appropriate bounds at the receiving layer and avoid logging whole rejected payloads. A malformed-syntax handler should catch the relevant FormatException without hiding unrelated application defects. Test wrong top-level types, missing and null fields, mixed lists, boundary numbers, and the normalization policy in a compatible isolated project. Here, JSON fixtures can be checked as data, but Dart decoding, runtime promotion, and target-specific numeric behavior were not executed; outputs remain reasoned expectations.

Common errors and fixes

Valid JSON is accepted despite wrong field types.

Why it happens
Syntax decoding was mistaken for schema and domain validation.
How to fix it
Validate top-level shape, required field types, and semantic ranges explicitly.
Prevent it next time
Keep decoded values untrusted until a validated model is constructed.

An internal field appears in the public payload.

Why it happens
The encoder serialized convenience state instead of a selected output contract.
How to fix it
Build an explicit representation containing only declared public fields.
Prevent it next time
Review serialization alongside authorization, logging, and data-minimization needs.

An invalid count becomes zero and is reported as success.

Why it happens
A fallback concealed the distinction between malformed data and a permitted value.
How to fix it
Return explicit validation failure or structured partial results.
Prevent it next time
Require a declared policy before collapsing invalid input into defaults.

Check your understanding

1. Should count: "3" be automatically treated as count: 3?

Show worked solution

Only if the public contract deliberately permits that coercion and specifies its exact grammar, ranges, and error behavior. Otherwise reject the type mismatch. Silent coercion can hide sender defects and create inconsistent behavior across clients. Keep syntactic conversion separate from validation of the resulting count.

2. Why does validating id and count not authorize a real batch operation?

Show worked solution

It establishes only the shown structure and value rules. The caller’s identity, resource ownership, allowed action, current state, and concurrency invariant remain separate checks at the authoritative owner. A well-formed request can still be forbidden or stale. Do not let a local DTO constructor become an accidental substitute for resource authorization.

Key takeaways

  • Decode, validate, authorize, and execute are distinct boundaries.
  • Explicit field and rejection policies preserve meaning across JSON interfaces.

Official references