Every sample is merged position by position before any Dart is written, so a list of tasks gives one class typed from all of them, and sound null safety states exactly what the samples showed. A key with a value in every sample is a final, non-nullable field and a required named parameter of the const constructor. One that is null in some sample is nullable, String?, but still required, because the API does send the key; one missing from some objects is nullable and optional, and toJson leaves it out while it is null, so a key that was missing stays missing. A cast is where a model breaks when the API changes: a null or a missing key for a non-nullable field fails with "type 'Null' is not a subtype of type 'String' in type cast".
Numbers are the classic trap: jsonDecode gives an int for 5 and a double for 5.0, and each fails a cast to the other: json['price'] as double throws "type 'int' is not a subtype of type 'double'" the first time a price is round. A field is int only when every sample is written as an integer, and otherwise double, read as (json['price'] as num).toDouble(), which accepts both. On the Dart VM an int holds 64 bits and a larger integer is read as a rounded double; on the web, compiled to JavaScript, an int is a JavaScript number, so integers beyond 2^53 lose digits, with a warning for each. Lists need care too: a decoded list is a List<dynamic> whatever it holds, so json['tags'] as List<String> fails even when every element is a string. So each list is read element by element, (json['tags'] as List<dynamic>).map((e) => e as String).toList(), and a list of objects calls the element class’s fromJson.
A string field is DateTime, when that option is on, only when every sample is an RFC 3339 date-time with a zone and at most six fraction digits. DateTime.parse accepts far more, including 2026-02-30, which it moves into March, and keeps only microseconds; toIso8601String() writes the same instant back in UTC, so +05:30 returns as Z. A date, or a time without a zone, stays String, as DateTime would read it as local time. Field names are lowerCamelCase in ASCII, the only letters Dart identifiers allow, so ünï gives uni, and a reserved word such as class or default, or a name the class already has, such as hashCode, toJson or int, gets Value added. With json_serializable each class has @JsonSerializable(), @JsonKey(name: …) only where the key differs from the field, @JsonKey(includeIfNull: false) on optional fields, and explicitToJson: true when it holds another model, so toJson returns nested maps rather than objects.