Serializzazione JSON avanzata in Flutter con json_serializable e build_runner
Best practiceIntermedio35 min Flutter 3.x

Serializzazione JSON avanzata in Flutter con json_serializable e build_runner

Perché automatizzare la serializzazione JSON?

Quando la tua app comunica con delle API, devi convertire continuamente oggetti Dart in JSON e viceversa. Scrivere a mano i metodi fromJson e toJson è noioso, ripetitivo e soggetto a errori, soprattutto con modelli complessi e nidificati.

In questo tutorial vedremo come usare i pacchetti json_serializable e build_runner per generare automaticamente il codice di serializzazione. Copriremo la configurazione, i campi rinominati, gli oggetti annidati, le liste e i valori di default. Al termine avrai un flusso di lavoro solido e manutenibile per gestire il JSON nelle tue app.

  1. 1

    Aggiungere le dipendenze al progetto

    Per prima cosa aggiungiamo le dipendenze necessarie. json_annotation è una dipendenza di runtime che fornisce le annotazioni, mentre json_serializable e build_runner sono dipendenze di sviluppo usate solo per generare il codice.

    Apri il file pubspec.yaml e aggiungi le righe seguenti, oppure esegui i comandi da terminale:

    flutter pub add json_annotation
    flutter pub add dev:build_runner
    flutter pub add dev:json_serializable
    
    dependencies:
      flutter:
        sdk: flutter
      json_annotation: ^4.9.0
    
    dev_dependencies:
      build_runner: ^2.4.13
      json_serializable: ^6.9.0

    Risultato atteso

    Eseguendo `flutter pub get` le dipendenze vengono scaricate senza errori.

  2. 2

    Creare il modello con le annotazioni

    Creiamo il file lib/models/user.dart. La struttura chiave prevede tre elementi:

    1. La direttiva part 'user.g.dart'; che collega il file generato.
    2. L'annotazione @JsonSerializable() sopra la classe.
    3. I due metodi fromJson e toJson che delegano alle funzioni generate.

    Non preoccuparti se l'IDE segnala un errore su user.g.dart: quel file non esiste ancora e verrà creato nel passo successivo.

    import 'package:json_annotation/json_annotation.dart';
    
    part 'user.g.dart';
    
    @JsonSerializable()
    class User {
      final int id;
      final String name;
      final String email;
    
      User({
        required this.id,
        required this.name,
        required this.email,
      });
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    
      Map<String, dynamic> toJson() => _$UserToJson(this);
    }

    Risultato atteso

    Il file compila a parte l'errore temporaneo su `_$UserFromJson` e `_$UserToJson`.

  3. 3

    Generare il codice con build_runner

    Ora avviamo la generazione del codice. Esegui questo comando dalla radice del progetto:

    dart run build_runner build --delete-conflicting-outputs
    

    L'opzione --delete-conflicting-outputs rimuove eventuali file generati obsoleti evitando conflitti.

    Durante lo sviluppo puoi anche usare la modalità watch, che rigenera automaticamente i file a ogni modifica:

    dart run build_runner watch --delete-conflicting-outputs
    
    dart run build_runner build --delete-conflicting-outputs

    Risultato atteso

    Viene creato il file `lib/models/user.g.dart` e gli errori nel modello scompaiono.

  4. 4

    Personalizzare i nomi dei campi

    Le API spesso usano convenzioni di naming diverse da quelle di Dart (ad esempio snake_case). Con @JsonKey(name: ...) puoi mappare un campo JSON su una proprietà con nome diverso.

    In alternativa, per convertire automaticamente tutte le chiavi in snake_case, puoi passare fieldRename: FieldRename.snake all'annotazione @JsonSerializable, evitando di annotare ogni singolo campo.

    Dopo ogni modifica ricorda di rieseguire build_runner.

    @JsonSerializable(fieldRename: FieldRename.snake)
    class User {
      final int id;
      final String name;
      final String email;
    
      // Mappato manualmente sul campo JSON "avatar_url"
      @JsonKey(name: 'avatar_url')
      final String? avatarUrl;
    
      User({
        required this.id,
        required this.name,
        required this.email,
        this.avatarUrl,
      });
    
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
      Map<String, dynamic> toJson() => _$UserToJson(this);
    }

    Risultato atteso

    Il JSON con chiavi come `avatar_url` viene deserializzato correttamente in `avatarUrl`.

  5. 5

    Gestire oggetti annidati e liste

    Il vero vantaggio di json_serializable emerge con i modelli complessi. Se un modello contiene altri oggetti annotati con @JsonSerializable(), la libreria gestisce automaticamente la serializzazione ricorsiva, incluse le liste di oggetti.

    Creiamo un modello Post che contiene un User come autore e una lista di Comment. Basta che anche Comment sia una classe annotata: non serve scrivere alcuna logica di conversione manuale.

    import 'package:json_annotation/json_annotation.dart';
    import 'user.dart';
    
    part 'post.g.dart';
    
    @JsonSerializable(explicitToJson: true)
    class Post {
      final int id;
      final String title;
      final User author;
      final List<Comment> comments;
    
      Post({
        required this.id,
        required this.title,
        required this.author,
        required this.comments,
      });
    
      factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
      Map<String, dynamic> toJson() => _$PostToJson(this);
    }
    
    @JsonSerializable()
    class Comment {
      final int id;
      final String text;
    
      Comment({required this.id, required this.text});
    
      factory Comment.fromJson(Map<String, dynamic> json) => _$CommentFromJson(json);
      Map<String, dynamic> toJson() => _$CommentToJson(this);
    }

    Risultato atteso

    Chiamando `post.toJson()` gli oggetti annidati vengono convertiti correttamente grazie a `explicitToJson: true`.

  6. 6

    Valori di default e campi calcolati

    Spesso le API restituiscono campi opzionali o nulli. Con @JsonKey(defaultValue: ...) puoi definire un valore da usare quando la chiave è assente o null, rendendo il modello più robusto.

    Puoi anche escludere campi calcolati dalla serializzazione con @JsonKey(includeFromJson: false, includeToJson: false), utile per proprietà derivate che non fanno parte del payload JSON.

    @JsonSerializable()
    class Product {
      final int id;
      final String name;
    
      @JsonKey(defaultValue: 0.0)
      final double price;
    
      @JsonKey(defaultValue: true)
      final bool available;
    
      // Campo calcolato: non incluso nel JSON
      @JsonKey(includeFromJson: false, includeToJson: false)
      String get displayLabel => '$name (\u20AC$price)';
    
      Product({
        required this.id,
        required this.name,
        required this.price,
        required this.available,
      });
    
      factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
      Map<String, dynamic> toJson() => _$ProductToJson(this);
    }

    Risultato atteso

    Se il JSON non contiene `price` o `available`, vengono usati i valori di default; `displayLabel` non compare nell'output di `toJson()`.

  7. 7

    Usare i modelli nella tua app

    Infine mettiamo tutto insieme. Tipicamente riceverai una stringa JSON da un'API: usa jsonDecode da dart:convert per ottenere una Map, poi passala al factory fromJson. Per l'invio, converti con toJson() e poi jsonEncode.

    Questo approccio ti garantisce codice generato, tipizzato e sempre allineato al modello, riducendo drasticamente i bug legati al parsing manuale.

    import 'dart:convert';
    import 'models/user.dart';
    
    void main() {
      // Deserializzazione da stringa JSON
      const jsonString = '{"id": 1, "name": "Anna", "email": "anna@example.com"}';
      final Map<String, dynamic> map = jsonDecode(jsonString);
      final user = User.fromJson(map);
      print('Utente: ${user.name}');
    
      // Serializzazione verso stringa JSON
      final newUser = User(id: 2, name: 'Luca', email: 'luca@example.com');
      final encoded = jsonEncode(newUser.toJson());
      print(encoded);
    }

    Risultato atteso

    In console vengono stampati 'Utente: Anna' e la stringa JSON dell'oggetto `newUser`.

CondividiXLinkedInFacebookWhatsApp

Commenti (0)

Ancora nessun commento. Inizia tu!