# Models with freezed and JSON

> What a model needs to work well in the cache, how freezed and json_serializable provide it, and why a wrapper class needs StructurallyShareable to keep unchanged items.

**The problem.** The app's models are generated by freezed and parsed with
json_serializable, as in most Flutter codebases. The queries return them, and
everything seems to work. But the rows of a list screen rebuild on every
refetch, even when nothing in them changed. What does a model need so that
the cache can recognise what did not change?

**The answer.** Two things, both easy to get:

1. **Value equality.** `==` and `hashCode` over the fields. Structural sharing
   compares with `==`, and a class without it is never equal to its previous
   version, so every fetch counts as a change. freezed generates both.
2. **Taking part in sharing when a class wraps a collection.** A class is a
   *leaf*: it is kept whole or replaced whole. A `Page(items: [...])` in which
   one item changed gets new instances for *every* item. Implementing
   `StructurallyShareable` lets the walk go inside it.

## What freezed generates, written out

This is the shape freezed produces, written by hand. It is useful for seeing
what the cache relies on:

`lib/data/invoice.dart`:

```dart
/// What freezed generates, in essence — and what a model needs either way.
@immutable
class Invoice {
  const Invoice(
      {required this.id, required this.customer, required this.cents});

  factory Invoice.fromJson(Map<String, Object?> json) => Invoice(
        id: json['id']! as String,
        customer: json['customer']! as String,
        cents: json['cents']! as int,
      );

  final String id;
  final String customer;
  final int cents;

  @override
  bool operator ==(Object other) =>
      other is Invoice &&
      other.id == id &&
      other.customer == customer &&
      other.cents == cents;

  @override
  int get hashCode => Object.hash(id, customer, cents);
}
```

The freezed version of the same model, for freezed 3 and json_serializable:

`lib/data/invoice.dart`:

```dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'invoice.freezed.dart';
part 'invoice.g.dart';

@freezed
abstract class Invoice with _$Invoice {
  const factory Invoice({
    required String id,
    required String customer,
    required int cents,
  }) = _Invoice;

  factory Invoice.fromJson(Map<String, Object?> json) =>
      _$InvoiceFromJson(json);
}
```

The query parses at the edge, so the cache only ever holds typed models:

`lib/data/invoice_queries.dart`:

```dart
QueryObserverOptions<List<Invoice>> invoicesQuery(Dio dio) =>
    QueryObserverOptions(
      queryKey: QueryKey(<Object?>['invoices']),
      queryFn: (context) async {
        final response = await dio.get<List<Object?>>('/invoices');
        return <Invoice>[
          for (final json in response.data!)
            Invoice.fromJson(json! as Map<String, Object?>),
        ];
      },
    );
```

A `List<Invoice>` in the cache needs nothing more. Lists are shared element
by element, so a refetch where one invoice changed keeps every other
invoice's instance.

## A wrapper is a leaf

Wrap that list in a class, as a paged API suggests, and the sharing stops at
the class. The measurement:

`test/invoice_sharing_test.dart`:

```dart
final before = InvoiceList([invoice('i1', 100), invoice('i2', 200)]);
// A refetch in which only i2 changed:
final after = replaceEqualDeep(
  before,
  InvoiceList([invoice('i1', 100), invoice('i2', 250)]),
);
// i1 is equal, but it is not the instance the cache held:
expect(identical(after.items[0], before.items[0]), isFalse);
```

Nothing looks broken. `==` still holds for `i1`, and the screen shows the
right data. What is lost is `identical`, and with it every rebuild skipped
because an item is the same instance as before.

## Letting the walk inside

Implement `StructurallyShareable` and hand the walk the list:

`lib/data/invoice_page.dart`:

```dart
@immutable
class InvoicePage implements StructurallyShareable<InvoicePage> {
  const InvoicePage({required this.items, required this.total});

  final List<Invoice> items;
  final int total;

  @override
  InvoicePage shareWith(InvoicePage previous) => InvoicePage(
        items: replaceEqualDeep(previous.items, items),
        total: total,
      );

  @override
  bool operator ==(Object other) =>
      other is InvoicePage &&
      other.total == total &&
      listEquals(other.items, items);

  @override
  int get hashCode => Object.hash(total, Object.hashAll(items));
}
```

With freezed, a private constructor lets the class have members of its own.
`copyWith` then keeps every other field as it is:

`lib/data/invoice_page.dart`:

```dart
@freezed
abstract class InvoicePage with _$InvoicePage
    implements StructurallyShareable<InvoicePage> {
  const InvoicePage._();

  const factory InvoicePage({
    required List<Invoice> items,
    required int total,
  }) = _InvoicePage;

  factory InvoicePage.fromJson(Map<String, Object?> json) =>
      _$InvoicePageFromJson(json);

  @override
  InvoicePage shareWith(InvoicePage previous) =>
      copyWith(items: replaceEqualDeep(previous.items, items));
}
```

`shareWith` is only called when the two pages are not equal, so it never
has to check for "nothing changed" itself.

## Steps

1. Give every model value equality: freezed, `equatable`, or `==` and
   `hashCode` written by hand.
2. Parse JSON in the query function and cache typed models, never
   `Map<String, dynamic>`.
3. Cache lists directly where you can. They are shared element by element
   with no further work.
4. For a class that wraps a collection, implement `StructurallyShareable`,
   and check once, as in the measurement above, that an unchanged item keeps
   its instance.

## Traps

- **Comparing freezed collections by identity.** freezed returns its lists
  wrapped in an unmodifiable view, and the view can be a new object on each
  access. Compare the items with `identical`, or the lists with
  `listEquals`. Do not compare `page.items` itself with `identical`.
- **Mutable models.** A model with a setter that changes a field in place
  changes the cached instance, and nothing notifies anyone. Keep models
  immutable, as freezed does by default, and write changes through
  `setQueryData` or `updateQueryData`.
- **Caching raw JSON.** A `Map<String, dynamic>` is kept whole or replaced
  whole, so one changed field replaces everything, and every reader has to
  parse. Parse once in the query function.
- **A `shareWith` that returns `previous` when something changed.** The
  cache then holds old data and nothing reports it. Build a new value from
  the shared parts, as above.

## Variations

- **A `sealed` result family.** A freezed union (`Loaded`, `Empty`) where
  one variant wraps a list implements `StructurallyShareable` on the base
  type. The walk asks only when both values have the same runtime type, so
  `shareWith` always sees two of the same variant, and a change of variant
  replaces the value whole.
- **A normalised response.** For `byId` maps, see [Normalised data or one
  key per entity](https://dualmeta-gmbh.github.io/query_kit/docs/cookbook/normalised-vs-per-entity-keys.md).
- **Turning sharing off.** For very large payloads that always change, see
  [Structural sharing](https://dualmeta-gmbh.github.io/query_kit/docs/guides/structural-sharing.md).

## See it run

In the select and structural sharing demo, five readers show one cache
entry, and each counts its *data builds*: the builds where its value was not
`==` to the one before. Press *Refetch*. Equal data comes back, and no count
moves. Press *Rename todo 2*, and only the reader that holds the list of texts
moves, with the control reader that has no `select`.
Then switch *Structural sharing off* and press *Refetch* again. The data is
equal, but the list is now a new instance, and the readers that hold a list
count a build for nothing. A wrapper class without `StructurallyShareable`
has the same effect on every item inside it.

Live demo: [Select and structural sharing](https://dualmeta-gmbh.github.io/query_kit/demo/showcase/#/select-and-sharing), running in the browser against an in-memory backend ([source](https://github.com/dualmeta-gmbh/query_kit/tree/main/examples/showcase/lib/features/select_and_sharing)). What a reader rebuilds on, and what it does not.

> **Note: In React Query**
>
> TanStack Query's structural sharing walks plain objects and arrays, which
> covers most JSON responses. A class instance is a leaf there too. The
> difference is that Dart models are always classes, so the wrapper case that
> is rare in JavaScript is common here, and `StructurallyShareable` is how a
> class takes part. See [differences from TanStack
> Query](https://dualmeta-gmbh.github.io/query_kit/docs/reference/differences-from-tanstack.md).
