Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Convertir Un String Json a Estructura con Rust

En el post anterior (Convertir un String en formato JSON a un objeto con Rust), hemos utilizando el crate serde_json para convertir un String que presenta un formato JSON a un objeto que se puede procesar utilizando Rust.

Para lograr transformar el String de nuestro ejemplo anterior, utilizamos una estructura del crate serde_json llamada Value, lo cual era bastante útil porque nos permite obtener todos los valores utilizando el patrón key:value y hemos logrado inclusive obtener cálculos, sin embargo, una desventaja que hemos observado es que, al no tener una manera explícita de definir los tipos de datos dentro del value, necesitamos utilizar un unwrap para ver si Rust puede o no obtener el valor en el tipo de dato que deseamos.

Lo anterior no es muy óptimo desde la perspectiva de Rust, puesto que una de las grandes ventajas de tener los datos con sus tipos respectivos es la óptima utilización de la memoria. Una alternativa para resolver este problema es procesar nuestro JSON con Value y luego pasarlo a una estructura, una gran alternativa para este propósito es utilizar un framework ya construido llamado serde 😁

¿Qué es serde y para qué sirve?

Serde, se define a sí mismo como "un framework para serializar y deserializar estructuras de datos de Rust de manera eficiente y genérica.", ¿te acuerdas de la solución que comentamos justo en el párrafo anterior? pues serde es justo lo que necesitamos 💪.

En crates.io, puedes encontrar el crate de serde dando click en este enlace. En la teoría, esto suena bastante bien, pero ahora vamos a otro aspecto que es super importante y es ¿cómo utilizamos serde?

Ejemplo utilizando serde y serde_json.

En nuestro post anterior teníamos un JSON el cual describe un libro y los precios de ese libro dependiendo si era en formato digital o tapa dura.

        
        
            // Representación de un Libro en formato JSON del post anterior.

{
  "titulo": "The Pragmatic Programmer",
  "autores": [
    "David Thomas",
    "Andrew Hunt"
  ],
  "total_paginas": 352,
  "generos": [
    "programacion",
    "ingenieria",
    "educacion"
  ],
  "precios": [
    {
      "tipo": "digital",
      "precio": 15.00,
      "moneda": "USD"
    },
    {
      "tipo": "tapa dura",
      "precio": 35.50,
      "moneda": "USD"
    }
  ]
}
        
        
    

En el código del post anterior, que puedes encontrar en el repositorio dando click a este enlace, vas a notar que cuando pasamos el String a un objeto de tipo "Value" (serde_json), al momento de intentar procesarlo debemos realizar distintas operaciones y llamados a unwrap() para poder extraer los valores que deseamos.

Para optimizar lo que hemos hecho anteriormente, vamos a repetir nuestro ejemplo, pero ahora haciéndolo utilizando serde.

Empecemos creando un nuevo proyecto:

cargo new json-a-estructura

cd json-a-estructura

(puedes ver el código completo en nuestro repositorio dando click a este enlace)

Lo primero que vamos a hacer es agregar a nuestro archivo Cargo.toml o utilizando cargo-edit los crates serde_json y serde, pero algo importante a tomar en cuenta, es que vamos a activar los macros derive de Serialize y Deserialize.

(Disclaimer: un macro derive tiene características similares a lo que en otros lenguajes se les conoce como decoradores, no son del todo iguales, pero es una analogía simple para el propósito de nuestro ejemplo).

Nuestro archivo Cargo.toml debe verse como el siguiente:

        
        
            // Cargo.toml

[package]
name = "json-a-estructura"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde_json = "1.0.82"
serde = { version = "1.0.138", features = ["derive"] }

        
        
    

Fíjate bien en la última línea de Cargo.toml, ya que estamos activando el feature para poder utilizar macro derive Serialize y Deserialize, si no lo incluyes, entonces el compilador marcará un error en tiempo de compilación y no podremos elaborar el ejecutable.

Ahora vamos a ir armando de a poco nuestro archivo main.rs.

Lo primero que vamos a hacer es indicar que vamos a utilizar los macro derive de serde y también serde_json.

        
        
            // main.rs

use serde_json::Result;
use serde::{Serialize, Deserialize};

        
        
    

Lo segundo que haremos será definir una estructura que describa un Libro y nuevamente utilizaremos el mismo String, solamente que esta vez crearemos un objeto de tipo "Libro", en lugar de tipo "Value", como lo hicimos anteriormente.

        
        
            // main.rs

use serde_json::Result;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct  Libro {
    titulo: String,
    total_paginas: u32,
    autores: Vec<String>,
    generos: Vec<String>,
}


        
        
    

Si te fijas, no hemos colocado los precios, y esto fue intencionado, ya que quiero mostrarte una funcionalidad bastante buena de serde.

La estructura que hemos creado (Libro), contiene atributos que comparten los atributos de nuestro String en JSON, continuemos con nuestro main.rs

        
        
            // main.rs

use serde_json::Result;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct  Libro {
    titulo: String,
    total_paginas: u32,
    autores: Vec<String>,
    generos: Vec<String>,
}

fn main() -> Result<()> {

    let libro = r#"{
  "titulo": "The Pragmatic Programmer",
  "autores": [
    "David Thomas",
    "Andrew Hunt"
  ],
  "total_paginas": 352,
  "generos": [
    "programacion",
    "ingenieria",
    "educacion"
  ],
  "precios": [
    {
      "tipo": "digital",
      "precio": 15.00,
      "moneda": "USD"
    },
    {
      "tipo": "tapa dura",
      "precio": 35.50,
      "moneda": "USD"
    }
  ]
}"#;

    let libro_parsed: Libro = serde_json::from_str(libro)?;

    println!("Libro procesado por Rust");
    println!("Titulo del libro: {}", libro_parsed.titulo);

    Ok(())
}

        
        
    

si ahora ejecutamos nuestro programa con:

cargo run

Veremos el siguiente resultado.

Es genial, nuestro programa se ejecuta perfectamente! 🥳

Sin embargo, si has leído el código a lo mejor te llamará la atención algunos detalles, por ejemplo, nuestra estructura Libro, no tiene la sección de precios, sin embargo el String en formato Json si lo tienen. A pesar de ello, serde ha sido capaz de hacer el parsing del string a nuestra estructura 🤯, este es una de las grandes virtudes de serde, que nos permite serializar/deserializar objetos y valida los atributos que están en el String y las que no. La línea que anteriormente pasaba nuestro JSON y un objeto Value, ha sido reemplazada para pasarlo directamente a nuestro objeto de tipo Libro.

        
        
            
let libro_parsed: Libro = serde_json::from_str(libro)?;
        
        
    

Y para obtener un valor, ya no es necesario utilizar el as_str.unwrap(), ahora podemos ingresar directamente al atributo de nuestro objeto 😎

        
        
            
println!("Titulo del libro: {}", libro_parsed.titulo);
        
        
    

Esto es lo que buscábamos, una forma óptima de procesar un Json con Rust!

Pero y ahora ¿cómo incluimos los precios?. Los precios en nuestro String, tienen este formato.

        
        
            

"precios": [
    {
      "tipo": "digital",
      "precio": 15.00,
      "moneda": "USD"
    },
    {
      "tipo": "tapa dura",
      "precio": 35.50,
      "moneda": "USD"
    }
  ]
        
        
    

Lo anterior podríamos convertirlo a una estructura similar a esta:

        
        
            
#[derive(Serialize, Deserialize, Debug)]
struct  Precio {
    precio: f32,
    tipo: String,
    moneda: String,
}


        
        
    

Y los precios podrían ser un vector de "Precio" dentro de nuestra estructura Libro, muy parecido a esto:

        
        
            
#[derive(Serialize, Deserialize, Debug)]
struct  Libro {
    titulo: String,
    total_paginas: u32,
    autores: Vec<String>,
    generos: Vec<String>,
    precios: Vec<Precio>
}


        
        
    

(Observa en como se define el atributo "precios")

Modifiquemos nuestro archivo main.rs y veamos si nuestra suposición es correcta y nos permite transformar el Json a un objeto de tipo Libro que incluya precios.

        
        
            // main.rs

use serde_json::Result;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct  Libro {
    titulo: String,
    total_paginas: u32,
    autores: Vec<String>,
    generos: Vec<String>,
    precios: Vec<Precio>
}

#[derive(Serialize, Deserialize, Debug)]
struct  Precio {
    precio: f32,
    tipo: String,
    moneda: String,
}

fn main() -> Result<()> {

    let libro = r#"{
  "titulo": "The Pragmatic Programmer",
  "autores": [
    "David Thomas",
    "Andrew Hunt"
  ],
  "total_paginas": 352,
  "generos": [
    "programacion",
    "ingenieria",
    "educacion"
  ],
  "precios": [
    {
      "tipo": "digital",
      "precio": 15.00,
      "moneda": "USD"
    },
    {
      "tipo": "tapa dura",
      "precio": 35.50,
      "moneda": "USD"
    }
  ]
}"#;

    let libro_parsed: Libro = serde_json::from_str(libro)?;

    println!("Libro procesado por Rust");
    println!("Titulo del libro: {}", libro_parsed.titulo);

    println!("PRECIOS:");
    for precio in libro_parsed.precios {
        println!("=====================================================================");
        println!("Tipo: {}", precio.tipo);
        println!("Precio: {}", precio.precio);
        println!("Moneda: {}", precio.moneda);
    }

    Ok(())
}

        
        
    

Si ejecutamos nuevamente nuestro programa con:

cargo run

Vamos a ver el siguiente resultado:

Perfecto! podemos utilizar estructuras más complejas que utilizando únicamente datos nativos de Rust, es de notar que ya no es necesario utilizar el as_array.unwrap en la definición del loop y que hemos ingresado directamente a los atributos.

        
        
            

for precio in libro_parsed.precios {
        println!("=====================================================================");
        println!("Tipo: {}", precio.tipo);
        println!("Precio: {}", precio.precio);
        println!("Moneda: {}", precio.moneda);
    }


        
        
    

Ya que tenemos la facilidad de ingresar a los atributos, ahora agreguemos el 10% de descuento al precio regular como en nuestro ejemplo anterior.

Completemos nuestro main.rs

        
        
            // main.rs

use serde_json::Result;
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct  Libro {
    titulo: String,
    total_paginas: u32,
    autores: Vec<String>,
    generos: Vec<String>,
    precios: Vec<Precio>
}

#[derive(Serialize, Deserialize, Debug)]
struct  Precio {
    precio: f32,
    tipo: String,
    moneda: String,
}

fn main() -> Result<()> {

    let libro = r#"{
  "titulo": "The Pragmatic Programmer",
  "autores": [
    "David Thomas",
    "Andrew Hunt"
  ],
  "total_paginas": 352,
  "generos": [
    "programacion",
    "ingenieria",
    "educacion"
  ],
  "precios": [
    {
      "tipo": "digital",
      "precio": 15.00,
      "moneda": "USD"
    },
    {
      "tipo": "tapa dura",
      "precio": 35.50,
      "moneda": "USD"
    }
  ]
}"#;

    let libro_parsed: Libro = serde_json::from_str(libro)?;

    println!("Libro procesado por Rust");
    println!("Titulo del libro: {}", libro_parsed.titulo);

    const DESCUENTO: f32 = 10.00;

    println!("PRECIOS:");
    for precio in libro_parsed.precios {
        println!("=====================================================================");

        let precio_regular = precio.precio;
        let precio_descuento = precio_regular - (precio_regular * (DESCUENTO / 100.00));

        println!("Tipo: {}", precio.tipo);
        println!("Precio Regular: {} {}", precio_regular, precio.moneda);
        println!("Precio 10% Descuento: {} {}", precio_descuento, precio.moneda);
    }

    Ok(())
}

        
        
    

Nuevamente al ejecutar nuestro programa vamos a obtener un resultado similar a este ya con el 10% de descuento:

Como puedes ver, utilizar serde ha facilitado bastante el procesamiento del formato JSON, además que nos permite utilizar estructuras que nosotros mismos podemos ir definiendo de acuerdo a nuestras necesidades o requeirimientos.

Si quieres ver el código completo de nuestro ejemplo, puedes verlo acceder al repositorio en github dando click a este enlace.

Si esta publicación te ha sido de utilidad, por favor compártela con tus amigos y en tus redes sociales, en nuestra próxima publicación vamos a convertir una estructura a JSON.

No olvides dejar tus comentarios y también puedes proponer los temas que te gustaría conocer un poco más sobre Rust.

println!("Hasta la próxima!");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies