Saltar al contenido principal

Trabaja con datos JSON

Aprende sobre la deserialización JSON en Dart, incluyendo cómo usar `dart:convert`, `jsonDecode` y pattern matching para trabajar con datos JSON de la API de Wikipedia.

En este capítulo, aprenderás a trabajar con datos JSON (JavaScript Object Notation) en Dart. JSON es un formato común para el intercambio de datos en la web, y a menudo te encontrarás con él al trabajar con APIs. Aprenderás a convertir datos JSON en objetos Dart, lo que facilitará el trabajo con ellos en tu aplicación. Usarás la librería dart:convert, la función jsonDecode y pattern matching.

Qué lograrás

Crea clases de modelos de datos para datos JSON
Usa dart:convert para trabajar con datos JSON
Usa pattern matching para extraer datos de objetos JSON
Configura un espacio de trabajo multipaquete

Prerrequisitos

#

Antes de comenzar este capítulo, asegúrate de:

  • Haber completado el Capítulo 8 y tener un entorno de desarrollo de Dart en funcionamiento con el proyecto dartpedia.
  • Entender la sintaxis básica de Dart, incluyendo clases y tipos de datos.

Tareas

#

En este capítulo, crearás clases Dart para representar los datos JSON devueltos por la API de Wikipedia. Esto te permitirá acceder y usar fácilmente los datos en tu aplicación.

Tarea 1: Crear el paquete Wikipedia

#

Primero, crea un nuevo paquete Dart para albergar los modelos de datos.

  1. Navega al directorio raíz de tu proyecto (/dartpedia).

  2. Ejecuta el siguiente comando en tu terminal:

    bash
    dart create wikipedia
    

    Este comando crea un nuevo directorio llamado wikipedia con la estructura básica de un paquete Dart. Ahora deberías ver una nueva carpeta wikipedia en la raíz de tu proyecto, junto con cli y command_runner.

Tarea 2: Configurar un espacio de trabajo de Dart

#

Los workspaces de Dart te permiten gestionar múltiples paquetes relacionados dentro de un solo proyecto, simplificando la gestión de dependencias y el desarrollo local. Ahora que estás agregando tu tercer paquete, es un buen momento para configurar tu proyecto para usar un workspace de Dart.

  1. Create the root pubspec.yaml file.

    Navega al directorio raíz de tu proyecto (/dartpedia) y crea un nuevo archivo llamado pubspec.yaml con el siguiente contenido:

    yaml
    name: _
    publish_to: none
    
    environment:
      sdk: ^3.8.1 # IMPORTANT: Adjust this to match your Dart SDK version or a compatible range
    workspace:
      - cli
      - command_runner
      - wikipedia
    
  2. Añadir resolución de workspace a los subpaquetes.

    Para cada uno de tus subpaquetes (cli, command_runner y wikipedia), abre sus respectivos archivos pubspec.yaml y añade resolution: workspace a pubspec.yaml. Esto le indica a Dart que resuelva las dependencias dentro del workspace.

    • Para cli/pubspec.yaml:

      yaml
      # ... (existing content) ...
      name: cli
      description: A sample command-line application.
      version: 1.0.0
      resolution: workspace # Add this line
      # ... (existing content) ...
      
    • Para command_runner/pubspec.yaml:

      yaml
      # ... (existing content) ...
      name: command_runner
      description: A starting point for Dart libraries or applications.
      version: 1.0.0
      resolution: workspace # Add this line
      # ... (existing content) ...
      
    • Para wikipedia/pubspec.yaml:

      yaml
      # ... (existing content) ...
      name: wikipedia
      description: A sample command-line application.
      version: 1.0.0
      resolution: workspace # Add this line
      # ... (existing content) ...
      

Tarea 3: Crear la clase Summary

#

La API de Wikipedia devuelve un objeto JSON que contiene un resumen de un artículo. Creemos una clase Dart para representar este resumen.

  1. Crea el directorio wikipedia/lib/src/model.

    bash
    mkdir -p wikipedia/lib/src/model
    
  2. Crea el archivo wikipedia/lib/src/model/summary.dart.

  3. Agrega el siguiente código a wikipedia/lib/src/model/summary.dart:

    wikipedia/lib/src/model/summary.dart
    dart
    import 'title_set.dart';
    
    class Summary {
      /// Returns a new [Summary] instance.
      Summary({
        required this.titles,
        required this.pageid,
        required this.extract,
        required this.extractHtml,
        required this.lang,
        required this.dir,
        this.url,
        this.description,
      });
    
      ///
      TitlesSet titles;
    
      /// The page ID
      int pageid;
    
      /// First several sentences of an article in plain text
      String extract;
    
      /// First several sentences of an article in simple HTML format
      String extractHtml;
    
      /// Url to the article on Wikipedia
      String? url;
    
      /// The page language code
      String lang;
    
      /// The page language direction code
      String dir;
    
      /// Wikidata description for the page
      String? description;
    
      /// Returns a new [Summary] instance
      static Summary fromJson(Map<String, Object?> json) {
        return switch (json) {
          {
            'titles': final Map<String, Object?> titles,
            'pageid': final int pageid,
            'extract': final String extract,
            'extract_html': final String extractHtml,
            'lang': final String lang,
            'dir': final String dir,
            'content_urls': {
              'desktop': {'page': final String url},
              'mobile': {'page': String _},
            },
            'description': final String description,
          } =>
            Summary(
              titles: TitlesSet.fromJson(titles),
              pageid: pageid,
              extract: extract,
              extractHtml: extractHtml,
              lang: lang,
              dir: dir,
              url: url,
              description: description,
            ),
          {
            'titles': final Map<String, Object?> titles,
            'pageid': final int pageid,
            'extract': final String extract,
            'extract_html': final String extractHtml,
            'lang': final String lang,
            'dir': final String dir,
            'content_urls': {
              'desktop': {'page': final String url},
              'mobile': {'page': String _},
            },
          } =>
            Summary(
              titles: TitlesSet.fromJson(titles),
              pageid: pageid,
              extract: extract,
              extractHtml: extractHtml,
              lang: lang,
              dir: dir,
              url: url,
            ),
          _ => throw FormatException('Could not deserialize Summary, json=$json'),
        };
      }
    
      @override
      String toString() =>
          'Summary['
          'titles=$titles, '
          'pageid=$pageid, '
          'extract=$extract, '
          'extractHtml=$extractHtml, '
          'lang=$lang, '
          'dir=$dir, '
          'description=$description'
          ']';
    }
    

    Este código define una clase Summary con propiedades que corresponden a los campos en la respuesta JSON de la API de Wikipedia. El método fromJson utiliza pattern matching para extraer los datos del objeto JSON y crear una nueva instancia de Summary. El método toString proporciona una forma conveniente de imprimir el contenido del objeto Summary. Ten en cuenta que la clase TitlesSet se utiliza en la clase Summary, por lo que necesitarás crearla a continuación.

Tarea 4: Crear la clase TitleSet

#

La clase Summary utiliza una clase TitlesSet para representar la información del título. Creemos esa clase ahora.

  1. Crea el archivo wikipedia/lib/src/model/title_set.dart.

  2. Agrega el siguiente código a wikipedia/lib/src/model/title_set.dart:

    wikipedia/lib/src/model/title_set.dart
    dart
    class TitlesSet {
      /// Returns a new [TitlesSet] instance.
      TitlesSet({
        required this.canonical,
        required this.normalized,
        required this.display,
      });
    
      /// the DB key (non-prefixed), e.g. may have _ instead of spaces,
      /// best for making request URIs, still requires Percent-encoding
      String canonical;
    
      /// the normalized title (https://www.mediawiki.org/wiki/API:Query#Example_2:_Title_normalization),
      /// e.g. may have spaces instead of _
      String normalized;
    
      /// the title as it should be displayed to the user
      String display;
    
      /// Returns a new [TitlesSet] instance and imports its values from a JSON map
      static TitlesSet fromJson(Map<String, Object?> json) {
        if (json case {
          'canonical': final String canonical,
          'normalized': final String normalized,
          'display': final String display,
        }) {
          return TitlesSet(
            canonical: canonical,
            normalized: normalized,
            display: display,
          );
        }
        throw FormatException('Could not deserialize TitleSet, json=$json');
      }
    
      @override
      String toString() =>
          'TitlesSet['
          'canonical=$canonical, '
          'normalized=$normalized, '
          'display=$display'
          ']';
    }
    

    Este código define una clase TitlesSet con propiedades que corresponden a la información del título en la respuesta JSON de la API de Wikipedia. El método fromJson utiliza pattern matching para extraer los datos del objeto JSON y crear una nueva instancia de TitlesSet. El método toString proporciona una forma conveniente de imprimir el contenido del objeto TitlesSet.

Tarea 5: Crear la clase Article

#

La API de Wikipedia también devuelve una lista de artículos en un resultado de búsqueda. Creemos una clase Dart para representar un artículo.

  1. Crea el archivo wikipedia/lib/src/model/article.dart.

  2. Agrega el siguiente código a wikipedia/lib/src/model/article.dart:

    wikipedia/lib/src/model/article.dart
    dart
    class Article {
      Article({required this.title, required this.extract});
    
      final String title;
      final String extract;
    
      static List<Article> listFromJson(Map<String, Object?> json) {
        final List<Article> articles = <Article>[];
        if (json case {'query': {'pages': final Map<String, Object?> pages}}) {
          for (final MapEntry<String, Object?>(:Object? value) in pages.entries) {
            if (value case {
              'title': final String title,
              'extract': final String extract,
            }) {
              articles.add(Article(title: title, extract: extract));
            }
          }
          return articles;
        }
        throw FormatException('Could not deserialize Article, json=$json');
      }
    
      Map<String, Object?> toJson() => <String, Object?>{
        'title': title,
        'extract': extract,
      };
    
      @override
      String toString() {
        return 'Article{title: $title, extract: $extract}';
      }
    }
    

    Este código define una clase Article con propiedades para el título y el extracto de un artículo. El método listFromJson utiliza pattern matching para extraer los datos del objeto JSON y crear una lista de instancias de Article. El método toJson convierte el objeto Article de nuevo en un objeto JSON. El método toString proporciona una forma conveniente de imprimir el contenido del objeto Article.

Tarea 6: Crear la clase SearchResults

#

Finalmente, creamos una clase para representar los resultados de búsqueda de la API de Wikipedia.

  1. Crea el archivo wikipedia/lib/src/model/search_results.dart.

  2. Agrega el siguiente código a wikipedia/lib/src/model/search_results.dart:

    wikipedia/lib/src/model/search_results.dart
    dart
    class SearchResult {
      SearchResult({required this.title, required this.url});
      final String title;
      final String url;
    }
    
    class SearchResults {
      SearchResults(this.results, {this.searchTerm});
      final List<SearchResult> results;
      final String? searchTerm;
    
      static SearchResults fromJson(List<Object?> json) {
        final List<SearchResult> results = <SearchResult>[];
        if (json case [
          String searchTerm,
          Iterable articleTitles,
          Iterable _,
          Iterable urls,
        ]) {
          final List titlesList = articleTitles.toList();
          final List urlList = urls.toList();
          for (int i = 0; i < articleTitles.length; i++) {
            results.add(SearchResult(title: titlesList[i], url: urlList[i]));
          }
          return SearchResults(results, searchTerm: searchTerm);
        }
        throw FormatException('Could not deserialize SearchResults, json=$json');
      }
    
      @override
      String toString() {
        final StringBuffer pretty = StringBuffer();
        for (final SearchResult result in results) {
          pretty.write('${result.url} \n');
        }
        return '\nSearchResults for $searchTerm: \n$pretty';
      }
    }
    

    Este código define una clase SearchResults con una lista de objetos SearchResult y un término de búsqueda. El método fromJson utiliza pattern matching para extraer los datos del objeto JSON y crear una nueva instancia de SearchResults. El método toString proporciona una forma conveniente de imprimir el contenido del objeto SearchResults.

En este punto, has creado modelos de datos para representar estructuras JSON. No hay nada que probar en este punto. Agregarás la lógica de la aplicación en las próximas secciones, lo que te permitirá probar cómo se deserializan los datos de la API de Wikipedia.

Revisión

#

Qué lograste

Aquí tienes un resumen de lo que construiste y aprendiste en esta lección.
Creaste clases de modelos de datos para JSON

Construiste las clases Summary, TitlesSet, Article y SearchResults para representar las respuestas de la API de Wikipedia. Estos modelos tipados proporcionan seguridad en tiempo de compilación y soporte de IDE al trabajar con datos de la API.

Usaste dart:convert para trabajar con datos JSON

Importaste la librería dart:convert y usaste jsonDecode() para analizar (parsear) cadenas JSON en objetos Dart, incluyendo Map y List, con los que luego puedes trabajar programáticamente.

Usaste pattern matching para extraer datos de objetos JSON

Implementaste métodos factory fromJson usando el pattern matching de Dart con expresiones switch y la sentencia if case. Esta estructura te permitió validar la estructura JSON y extraer valores de los objetos JSON en expresiones únicas y legibles.

Configuraste un workspace de pub

Para simplificar la gestión de dependencias en tu proyecto multipaquete, creaste un nuevo workspace de pub. Para hacerlo, creaste un archivo pubspec.yaml raíz con una sección workspace: que lista tus paquetes, luego agregaste resolution: workspace a cada subpaquete.

Cuestionario

#

Pon a prueba tu comprensión

1 / 3
What is JSON?
  1. Un lenguaje de programación diseñado específicamente para el desarrollo web.

    No exactamente.

    JSON no es un lenguaje de programación. No puedes escribir lógica ni ejecutar código con JSON.

  2. Una librería específica de Dart para manejar la serialización de datos.

    No exactamente.

    JSON no es específico de Dart ni es una librería. Existió antes que Dart y se usa en todas partes.

  3. Un formato de base de datos utilizado exclusivamente por aplicaciones de JavaScript.

    No exactamente.

    JSON no es un formato de base de datos, y no es exclusivo de JavaScript. Aunque se originó a partir de la sintaxis de JavaScript, se utiliza en muchos lenguajes y plataformas.

  4. Un formato ligero de intercambio de datos que es fácil de leer y escribir para los humanos y fácil de parsear y generar para las máquinas.

    ¡Correcto!

    JSON (JavaScript Object Notation) es un formato basado en texto para representar datos estructurados. Se usa ampliamente para el intercambio de datos entre aplicaciones, especialmente en APIs web.

When jsonDecode parses a JSON object like {"name": "Dart"}, what Dart type does it return?
  1. Un List<String> que contiene los pares clave-valor.

    No exactamente.

    Una lista perdería la relación clave-valor. Los objetos JSON tienen propiedades con nombre, lo que requiere una estructura de datos diferente a una lista.

  2. Un String que contiene el texto JSON.

    No exactamente.

    jsonDecode analiza (parsea) la cadena JSON en objetos Dart. Convierte la representación en cadena a datos estructurados, incluyendo mapas y listas, sin mantenerla como una cadena.

  3. Una clase JsonObject personalizada de dart:convert.

    No exactamente.

    No existe una clase JsonObject en Dart. jsonDecode devuelve tipos de colecciones estándar de Dart.

  4. Un Map<String, dynamic> que representa al objeto JSON.

    ¡Correcto!

    Los objetos JSON se convierten en Map<String, dynamic>, donde las claves son cadenas y los valores pueden ser cualquier tipo compatible con JSON.

Why create a Summary class with a fromJson factory constructor instead of just using the raw Map from jsonDecode?
  1. Dart requiere que todos los datos JSON se conviertan a clases antes de usarse.

    No exactamente.

    Dart no requiere esto. Puedes usar mapas directos directamente en todo tu código.

  2. Seguridad de tipos, autocompletado en el IDE y código más claro que documenta la estructura de datos esperada.

    ¡Correcto!

    Una clase te proporciona comprobación de tipos en tiempo de compilación, autocompletado para propiedades y código autodocumentado. summary.title es más claro que map['title'].

  3. jsonDecode no puede parsear objetos JSON anidados sin una clase.

    No exactamente.

    jsonDecode maneja bien los objetos anidados, que se convierten en mapas anidados. Las clases sirven para un propósito diferente al análisis (parsing).

  4. Los mapas directos (raw maps) son más lentos de acceder que las propiedades de una clase.

    No exactamente.

    El rendimiento no es la razón principal. El acceso a mapas es bastante rápido en Dart. Piensa en la experiencia del desarrollador en su lugar.

Siguiente lección

#

En la siguiente lección, aprenderás a probar tu código Dart usando la librería package:test. Escribirás pruebas para asegurarte de que tu lógica de deserialización JSON esté funcionando correctamente.