[dart-dio] Skip null optional form params and JSON-encode model form params

AR
15h ago
+147-6

Fixes #10295

Problem

For multipart/form-data and application/x-www-form-urlencoded operations, the dart-dio api/serialize.mustache templates put every form param into the map passed to Dio. Dio stringifies every value that isn't a MultipartFile, which causes two bugs:

  1. Unset nullable params are sent as "" (all three serialization libraries). The if (x != null) guard is only emitted for non-nullable optional params ({{^required}}{{^isNullable}}…{{/isNullable}}{{/required}}). A nullable optional param that the caller leaves unset is still added to the map, and Dio sends it as an empty value (note= in multipart, a bare note key in url-encoded bodies). That can overwrite stored values with blanks (e.g. sending an empty password, as reported in #10295).
  2. Model params are sent as toString() (json_serializable and freezed). A model or List<Model> form param is passed through as-is, so the server receives the Dart toString() (Asset(id: a1)) instead of JSON. built_value is not affected, because it already encodes models through encodeFormParameter.

Fix

  • built_value, json_serializable, freezed: guard every non-required form param with if (x != null), whether or not it is nullable. Required params are always sent, as before.
  • json_serializable, freezed: wrap model and array-of-model form params in jsonEncode(...). With explicit_to_json: true (set in both generated build.yamls), toJson() already returns plain JSON, so no toEncodable is needed. uniqueItems model arrays are generated as Set<Model>, which jsonEncode cannot encode (JsonUnsupportedObjectError), so they are converted with .toList() first. Primitives and files are unchanged.

Generated code for an optional nullable string note, a model assets and a list of models assetList:

// before
r'note': note,
if (assets != null) r'assets': assets,
if (assetList != null) r'assetList': assetList,

// after
if (note != null) r'note': note,
if (assets != null) r'assets': jsonEncode(assets),
if (assetList != null) r'assetList': jsonEncode(assetList),

Verification

Requests captured with a Dio interceptor from clients generated from the new test spec (form_params.yaml), built with build_runner and run:

requestlibrarybeforeafter
multipart, assets + assetList set, note unsetfreezednote= · assets=Asset(id: a1) · assetList=Asset(id: a2)assets={"id":"a1"} · assetList=[{"id":"a2"}]
json_serializablenote= · assets={id: a1} · assetList={id: a2}assets={"id":"a1"} · assetList=[{"id":"a2"}]
built_valuenote= (models already correct)note omitted
multipart, only nameall threename=only-name · note=name=only-name
url-encoded, assets setfreezedname=x&note&assets=Asset%28id%3A+a1%29name=x&assets=%7B%22id%22%3A%22a1%22%7D
url-encoded, only namefreezed, json_serializablename=only-name&notename=only-name
built_valuename=only-name&note=name=only-name
  • New tests in DartDioClientCodegenTest: testOptionalNullableFormParamsAreSkippedWhenNull (data-driven over all three libraries) and testModelFormParamsAreJsonEncoded. They fail on master and pass with this change.
  • The existing dart-dio samples regenerate unchanged: their spec has no nullable or model-typed form params.

Behaviour change

An unset nullable form param is now omitted instead of being sent as "". Callers that relied on "" to clear a field must now pass '' explicitly.

PR checklist

  • Read the contribution guidelines.
  • Ran ./mvnw clean package, ./bin/generate-samples.sh ./bin/configs/dart-dio-*.yaml and ./bin/utils/export_docs_generators.sh; committed all changed files (no sample changes).
  • File the PR against the master branch.
  • Technical committee: @jaumard @amondnet @sbu-WBT @kuhnroyal @agilob @ahmednfwela @wing328

Summary by cubic

Fixes dart-dio form-param serialization: unset nullable form params were sent as "" (see #10295), and json_serializable/freezed sent model form params as Dart toString() output instead of JSON. Optional form params are now omitted when null, and model and List<Model> form params are JSON-encoded. built_value only needed the null-skip fix since it already encodes models.

Behavior change

  • Unset nullable form params are now omitted instead of being sent as ""; callers that relied on "" to clear a field must pass '' explicitly.

Tests

  • New generated-code tests cover nullable skip and model JSON encoding across all three serialization libraries.