Rusty Full Stack
El blog para los amantes de Rust, Ionic y Vuejs
El blog para los amantes de Rust, Ionic y Vuejs
En este post iniciamos una nueva miniserie para el manejo de JSON. En esta primera parte vamos a hablar sobre JSON y cómo convertir una String que en formato JSON y crearemos un objeto procesable por Rust, también vamos a comentar un poco sobre los crates serde y serde_json las cuales nos facilitarán para dicha labor.
En los siguientes episodios vamos a hablar sobre:
Pero empezaremos de forma sencilla convirtiendo un String en formato JSON o un objeto procesable por Rust.
JSON es un acrónimo para (JavaScript Object Notation), en su documentación, se defino como un formato liviano para intercambio de datos y que tiene la característica de ser fácil de leer y escribir para los humanos y también es fácil para las máquinas de interpretar y generar.
El formato JSON es quizás, al momento de escribir este post, el más utilizado para el intercambio de datos gracias a las API's tanto las de back-end como las de front-end; si eres un desarrollador de software, es casi imposible que no te toque utilizarlo en algún momento de nuestra profesión, o por lo menos, que nos topemos con algún ejemplo o documentación de la cual debamos trabajar.
Para escribir y leer formato JSON, utilizaremos el patrón "llave":"valor", por ejemplo, supongamos que queramos describir utilizando JSON la descripción de un libro. Sabemos que un libro puede tener un título, uno o más autores, la cantidad de páginas, los géneros del libro, etc. Una forma de describir todas estas características puede ser la siguiente:
// JSON de un libro
{
"titulo": "The Pragmatic Programmer",
"autores": [
"David Thomas",
"Andrew Hunt"
],
"total_paginas": 352,
"generos": [
"programacion",
"ingenieria",
"educacion"
]
}
Hay que notar que empieza y termina con { }
, también, si debes especificar un listado, debes utilizar [ ]
al igual que cualquier array o lista en varios lenguajes de programación. Los valores tipo String, les agregamos las comillas, y puede o no ser opcional para los valores puramente numéricos. También hay que notar, que puedes anidar JSON con más JSON, por ejemplo si queremos agregar los precios si el libro es virtual o de tapa dura:
// JSON de un libro
{
"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"
}
]
}
Como puedes ver, escribir y leer JSON no es tan complicado y a la vez es bastante flexible en como lo queremos escribir, pero otro aspecto bastante importante es que también las máquinas lo sabe interpretar (con el ejemplo de arriba ya hasta podrías hacer una pantalla web de catálogo de libros 😎).
Vamos ahora a leer un formato JSON con Rust y lo vamos a convertir o serializar o un formato el cual nos permita, que el objeto que interpretemos, sea procesado por Rust y logremos realizar distintas operaciones o validaciones.
Los crates estrellas para el manejo de JSON con Rust son serde y serde_json.
Escribamos un programa para convertir nuestro ejemplo de los libros en un objeto en Rust.
Todos los códigos de ejemplo podrás encontrarlo en este repositorio de github.
Primero, creemos un nuevo proyecto.
cargo new json-con-rust.rs
En nuestro archivo Cargo.toml o con cargo-edit vamos a agregar en nuestras dependencias el crate serde_json, el cual nos proporciona todas las herramientas para convertir un String a JSON y que Rust lo pueda procesar
// Cargo.toml
[package]
name = "leer-json"
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"
Ahora vamos a ir armando nuestro archivo main.rs paso a paso, primero vamos a agregar los use statements.
// main.rs
use serde_json::{Result, Value};
En la línea anterior, estamos especificando que vamos a utilizar la estructura Value el cual es utilizado para crear un objeto de memory-safe para que Rust pueda interpretar el JSON que le pasemos, sin preocuparnos de los tipos de datos que contenga el JSON. Value, va a contener tanto las keys como los values de nuestro JSON, incluyendo arrays 😬!
Ahora vamos a agregar el mismo String que hemos creado para los Libros dentro de nuestra función main
// main.rs
use serde_json::{Result, Value};
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"
}
]
}"#;
println!("Libro sin procesar: {}", libro);
Ok(())
}
Algo importante a notar, es que el String libro lleva dentro de su valor esta notación que se conoce como un raw string
r#"<Algun String>"#
No es obligatorio que nuestro JSON sea un raw String, pero, al no utilizar esta notación, es importante considerar que las comillas dobles " " de los key:values pueden generarte algún problema y deberás tener ese cuidado por tu cuenta.
Si ejecutas el código anterior, verás que en tu consola se imprime el String de la descripción del libro que hemos colocado, todavía no hemos hecho que Rust lo procese. Ahora vamos a agregar esa funcionalidad y vamos a imprimir el título del libro pero utilizando el objeto resultante de tipo Value.
// main.rs
use serde_json::{Result, Value};
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"
}
]
}"#;
// Aca estamos procesando el JSON desde un String
let libro_parsed: Value = serde_json::from_str(libro)?;
println!("Libro procesado por Rust");
println!("Titulo del libro: {}", libro_parsed["titulo"]);
Ok(())
}
con la línea:
let libro_parsed: Value = serde_json::from_str(libro)?;
Estamos convirtiendo el String que representa nuestro JSON a un objeto tipo Value, lo que nos permitirá procesar su valores, un ejemplo de ese procesamiento la encontramos en esta línea:
println!("Titulo del libro: {}", libro_parsed["titulo"]);
libro_parsed ahora contiene todos los datos de nuestro JSON y podremos acceder a. ellos como si de un Hashmap o diccionario (para usuarios python 🐍) se tratase. El crate serde_json se ha encargado de procesar todos los tipos de datos por nosotros y para acceder a los atributos solamente necesitamos incluir entre [ ] el atributo que necesitamos (libro_parsed["titulo"])
Si ejecutamos nuestro programa con:
cargo run
Vamos a ver que en nuestra terminal imprime lo siguiente:
Genial! ahora nuestro programa ha convertido un String a formato JSON y lo estamos procesando con el objeto Value de serde_json, pero si has sido observador, hay un detalle importante a destacar, si te fijas en la línea
Titulo del libro: "The Pragmatic Programmer"
El título está entre " " y nosotros no lo hemos especificado en ninguna parte 😱
Esto ocurre porque el acceso directo a algún atributo nos devuelve el valor en crudo, y si vemos el atributo título de nuestra String, vemos que lleva las comillas. Si quieres quitárselas, modifiquemos un poco el código para tener el dato concreto que buscamos (sin comillas).
use serde_json::{Result, Value};
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: Value = serde_json::from_str(libro)?;
println!("Libro procesado por Rust");
println!("Titulo del libro: {}", libro_parsed["titulo"].as_str().unwrap());
Ok(())
}
La línea que ha cambiado es:
println!("Titulo del libro: {}", libro_parsed["titulo"].as_str().unwrap());
libro_parsed["titulo"].as_str().unwrap()
está indicando que convierta el valor a un str (si puede, por eso el unwrap 😜) y que procesemos el valor como un str, si volvemos a ejecutar nuestro código, ahora veremos que imprime el resultado pero sin las comillas dobles.
Algo importante a tener en cuenta, es que también podemos acceder a los array de nuestro JSON utilizando siempre nuestro objeto de tipo Value.
Continuando con nuestro ejemplo, vamos ahora a imprimir los precios del libro, pero para hacerlo más interesante y que veamos que podemos procesar los datos con Rust, vamos a imprimir tanto el precio normal y el precio con un 10% de descuento.
// main.rs
use serde_json::{Result, Value};
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: Value = serde_json::from_str(libro)?;
const DESCUENTO: f64 = 10.00; // Valor de descuento, puedes cambiarlo a tu necesidad.
println!("Libro procesado por Rust");
println!("Titulo del libro: {}", libro_parsed["titulo"].as_str().unwrap());
println!("PRECIOS:");
for precio in libro_parsed["precios"].as_array().unwrap() {
println!("=====================================================================");
let precio_regular = precio["precio"].as_f64().unwrap();
let precio_descuento = precio_regular - (precio_regular * (DESCUENTO / 100.00));
println!("Tipo: {}", precio["tipo"].as_str().unwrap());
println!("Precio Regular: {} {}", precio_regular, precio["moneda"]);
println!("Precio 10% Descuento: {} {}", precio_descuento, precio["moneda"]);
}
Ok(())
}
Si ejecutamos el código anterior con:
cargo run
Veremos que en nuestra consola tendremos el siguiente resultado con el listado de precios y el 10 de descuento 😃:
La parte importante a comentar es el siguiente bloque de código de main.rs
// loop al final de main.rs
for precio in libro_parsed["precios"].as_array().unwrap() {
println!("=====================================================================");
let precio_regular = precio["precio"].as_f64().unwrap();
let precio_descuento = precio_regular - (precio_regular * (DESCUENTO / 100.00));
println!("Tipo: {}", precio["tipo"].as_str().unwrap());
println!("Precio Regular: {} {}", precio_regular, precio["moneda"]);
println!("Precio 10% Descuento: {} {}", precio_descuento, precio["moneda"]);
}
En la declaración del for vamos a ver que estamos accediendo al atributo "precios", y estamos pidiendo a serde_json que lo convierta en un array, para este ejemplo, tenemos controlado nuestro código, por lo que le unwrap no fallaría, lo ideal sería tener los tipos definidos en una estructura (eso lo hacemos en el siguiente post 😇).
Y en las lineas:
let precio_regular = precio["precio"].as_f64().unwrap();
let precio_descuento = precio_regular - (precio_regular * (DESCUENTO / 100.00));
Vamos la gran utilidad de tener nuestro JSON en un objeto procesable por Rust, porque hemos logrado hacer cálculos con los datos de los precios del libro.
Espero que esta publicación te haya servido para saber como convertir un String en JSON a un objeto en Rust.
Recuerda que todos los ejemplos de esta nueva mini serie las podrás encontrar en nuestro repositorio en github.
En nuestra siguiente publicación vamos a realizar una mejora en nuestro código, para manejar tipos con mayor seguridad desde la perspectiva de Rust y que nos permitirá obviar algunos unwrap() que hemos estado realizando. Esto lo vamos a conseguir definiendo estructuras que reemplacen el uso directo de serde_json::Value y pasar desde un String directamente a nuestra propia estructura.
Nos vemos en el siguiente post.
println!("Hasta la próxima!!");