Skip to content
Volver al blog
Tutoriales

JSON a struct de Rust: generar structs serde (guía 2026)

Convierte JSON a structs de Rust correctamente: tipos numéricos (i64/u64/f64), Option para null, serde rename para camelCase y los errores a evitar. Gratis.

11 min de lectura

JSON a struct de Rust: generar structs serde (guía 2026)

Pega tu payload en el convertidor de JSON a struct de Rust y copia el struct serde que genera. Ese es todo el flujo de trabajo de JSON a struct de Rust: nada que instalar, nada que subir, todo ocurre en tu navegador. Cubre en segundos el caso de “necesito un struct ahora mismo”.

Sin embargo, generar un struct y generar el struct correcto son problemas distintos. Un convertidor solo puede adivinar. Y aquí Rust sube la apuesta. En TypeScript, una suposición equivocada se degrada a un any laxo que puedes ignorar. Con serde, una suposición equivocada falla en la deserialización: un float colocado en un i64, un null inesperado en un campo no opcional, y serde_json::from_str te devuelve un Err en lugar de tus datos. Por eso acertar con el struct importa más en Rust que en la mayoría de los lenguajes.

A continuación verás cómo funciona en realidad la inferencia, la regla de tipado numérico en la que la mayoría de los convertidores se equivoca, cuándo recurrir a #[serde(rename)] frente a #[serde(rename_all)], y cómo manejar fechas, claves dinámicas y palabras reservadas de Rust para que la salida siempre compile.

Cómo convertir JSON a Rust

Convertir JSON a Rust toma tres pasos:

  1. Pega tu JSON. Suelta un objeto, un arreglo o una respuesta cruda de API en el cuadro de entrada. La conversión se ejecuta al instante y por completo del lado del cliente.
  2. Ajusta la salida. Renombra el struct raíz, activa o desactiva los derives de serde, agrega Debug y Clone, o quita la visibilidad pub para que coincida con el estilo de tu crate.
  3. Copia o descarga. Toma el Rust generado con un clic y pégalo directamente en tu proyecto.

Ahí termina el camino transaccional. Si tu entrada está minificada o no estás seguro de que sea válida, pásala primero por un formateador JSON para que el convertidor trabaje con JSON limpio y bien formado. El resto de esta guía explica cómo leer la salida para que puedas corregir los casos que una herramienta no puede inferir por sí sola.

Cómo se mapean los tipos JSON a Rust

Cada valor JSON tiene su contraparte en Rust, pero el mapeo no es del todo uno a uno:

Valor JSONTipo de Rust
"text"String
42i64 (valores grandes u64, más allá f64)
3.14, 2e3f64
true / falsebool
nullOption<T> (o Option<serde_json::Value>)
[1, 2, 3]Vec<T>
{ ... }un struct con nombre

Los objetos son donde ocurre el trabajo. Toma un payload REST típico:

{
  "id": 101,
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "active": true,
  "roles": ["admin", "user"]
}

El convertidor produce un struct listo para serde:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

Los escalares se mapean a primitivos, y roles se convierte en Vec<String>. El único tipo JSON que no se mapea limpiamente es number: JSON tiene un solo tipo numérico, mientras que Rust te obliga a elegir entre i64, u64 y f64. Esa elección es el tema de toda una sección más abajo, porque es donde la mayoría de los generadores se equivoca.

Cómo infiere los structs el convertidor

Cuatro reglas cubren casi todo lo que le des: un struct por forma de objeto, fusión clave por clave para arreglos, tipado numérico preciso y nombres de campo idiomáticos.

Inferencia estructural: un struct con nombre por objeto

Cada forma de objeto distinta se convierte en su propio struct con nombre. Los objetos anidados no se incrustan; se elevan a definiciones separadas y referenciadas:

{ "repo": "serde", "owner": { "login": "dtolnay", "id": 100 } }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub repo: String,
    pub owner: Owner,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Owner {
    pub login: String,
    pub id: i64,
}

El owner anidado se convierte en su propio struct Owner, referenciado por campo. Las formas idénticas se deduplican, así que dos campos con la misma estructura comparten un único struct en lugar de producir copias.

Fusión de arreglos y campos Option

Cuando pasas un arreglo de objetos, el convertidor los fusiona clave por clave. Una clave presente en algunos elementos pero no en otros se convierte en un Option:

{ "users": [{ "id": 1, "nick": "x" }, { "id": 2 }] }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub users: Vec<User>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i64,
    pub nick: Option<String>,
}

id aparece en cada elemento, así que sigue siendo obligatorio. nick falta en el segundo usuario, así que se convierte en Option<String>. Fíjate en lo que no está aquí: nada de #[serde(default)]. serde ya trata Option como opcional y deserializa una clave ausente a None, así que el atributo sería redundante.

Tipado numérico correcto: i64, u64, f64

Esta es la regla que las herramientas de la competencia omiten, y es la que se rompe con payloads reales. El generador aplica tres niveles. Un entero se mapea a i64. Si un valor supera i64::MAX, asciende a u64. Más allá de u64::MAX, recurre a f64. Cualquier token escrito con punto decimal o exponente, como 1.0 o 2e3, se mapea a f64 sin importar su magnitud.

{ "user_id": 12000000000000000000, "balance": 19.99, "retries": 3 }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub user_id: u64,
    pub balance: f64,
    pub retries: i64,
}

Aquí user_id es mayor que i64::MAX, así que aterriza en u64 y sigue haciendo round-trip. balance lleva un punto decimal, así que es f64, no un entero. Esto importa porque serde impone el tipo en la deserialización. Dale un struct que tipa un ID estilo snowflake o Twitter como i32 y el valor se desborda; tipa un campo float como i64 y serde devuelve un error en lugar de truncar en silencio. La regla de tres niveles es lo que hace que un rust struct from json generado siga deserializando en vez de entrar en pánico con el primer ID grande.

Campos snake_case y #[serde(rename)]

Las claves JSON suelen estar en camelCase; los campos idiomáticos de Rust están en snake_case. El convertidor renombra el campo y agrega un #[serde(rename)] que lo mapea de vuelta a la clave JSON exacta:

{ "login": "octocat", "publicRepos": 15, "followerCount": 9001, "createdAt": "2011-01-25" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub login: String,
    #[serde(rename = "publicRepos")]
    pub public_repos: i64,
    #[serde(rename = "followerCount")]
    pub follower_count: i64,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

Una clave que ya está en snake_case, como login, no recibe rename. Las demás se mapean limpiamente de vuelta al formato de transmisión mientras tu código Rust se lee con naturalidad.

#[serde(rename)] vs #[serde(rename_all)]

La herramienta emite un #[serde(rename)] por campo porque siempre funciona, incluso cuando un payload mezcla camelCase, snake_case y claves irregulares en el mismo objeto. Nunca tiene que razonar sobre una convención compartida que quizá no exista.

Cuando todos los campos de un struct comparten una convención, puedes colapsar esos atributos en un único rename a nivel del contenedor. Elimina las líneas por campo y pon un solo #[serde(rename_all = "camelCase")] en el struct:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
    pub login: String,
    pub public_repos: i64,
    pub follower_count: i64,
    pub created_at: String,
}

Ambas versiones deserializan el mismo JSON. Usa rename por campo para claves mixtas o irregulares; recurre a rename_all cuando un struct es uniforme y quieres menos ruido. serde también incluye otras convenciones como "snake_case", "kebab-case" y "SCREAMING_SNAKE_CASE" para el mismo atributo.

Palabras reservadas de Rust y claves que no son identificadores

Las claves JSON son solo cadenas, así que nada impide que una API envíe una clave llamada type o match, ambas palabras reservadas en Rust. El generador las sanea en un identificador legal más un rename:

{ "type": "user", "match": true, "first-name": "Ada" }
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    #[serde(rename = "type")]
    pub type_: String,
    #[serde(rename = "match")]
    pub match_: bool,
    #[serde(rename = "first-name")]
    pub first_name: String,
}

La forma con guion bajo al final (type_) se elige deliberadamente en lugar de un identificador crudo como r#type. Los identificadores crudos no pueden expresar toda palabra reservada en toda posición; self, crate y super en particular son rechazados como identificadores crudos, así que un enfoque de saneo más rename es el único que siempre compila. Las claves que no son identificadores, como first-name o una 2fa que empieza con dígito, reciben el mismo tratamiento: un nombre Rust válido y un rename de vuelta a la clave JSON literal.

Deserializar con serde_json

Una vez que tengas el struct, agrega las dependencias a Cargo.toml:

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

Luego basta una sola línea para parsear. Este ejemplo está completo y compila tal cual:

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub id: i64,
    pub name: String,
    pub email: String,
    pub active: bool,
    pub roles: Vec<String>,
}

fn main() -> Result<(), serde_json::Error> {
    let data = r#"{"id":101,"name":"Ada Lovelace","email":"a@ex.com","active":true,"roles":["admin"]}"#;
    let root: Root = serde_json::from_str(data)?;
    println!("{} has {} role(s)", root.name, root.roles.len());
    Ok(())
}

El derive Deserialize hace todo el trabajo. Usa serde_json::from_str para una cadena, from_slice para un slice de bytes o from_reader para un archivo o cuerpo HTTP, y serde_json::to_string para serializar un valor de vuelta a JSON. Esta es la recompensa por acertar con los tipos: el serde derive que generaste es un validador en tiempo de ejecución, así que una llamada de deserialización de serde_json que devuelve Ok es una garantía de que los datos coincidieron con tu struct, no solo la creencia de un compilador de que lo hicieron.

Fechas, claves dinámicas y campos desconocidos

Algunas formas no se mapean a un campo de struct simple, y el generador les da un valor por defecto razonable que luego ajustas a mano.

Fechas. JSON no tiene tipo de fecha, así que una marca de tiempo ISO o RFC 3339 llega como un String simple. Para un manejo real de fechas, cambia el campo a un tipo de chrono y habilita la feature serde de chrono (chrono = { version = "0.4", features = ["serde"] }). Entonces serde parsea RFC 3339 automáticamente:

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
    pub name: String,
    pub created_at: DateTime<Utc>,
}

Claves dinámicas. Cuando las claves varían en tiempo de ejecución, como un mapa de IDs a valores, un struct fijo es la forma equivocada. Usa un HashMap con clave String:

use std::collections::HashMap;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Root {
    pub scores: HashMap<String, i64>,
}

Campos extra desconocidos. Para conservar un struct tipado pero aún así capturar claves que no modelaste, agrega un campo #[serde(flatten)] respaldado por un HashMap. Para un valor que es genuinamente dinámico, serde_json::Value es el comodín. Cuando lo que realmente necesitas es validar la estructura en lugar de generar tipos, la guía de validación con JSON Schema cubre cómo imponer un contrato de extremo a extremo.

json-to-rust vs quicktype vs typeshare vs manual

No hay una única mejor manera de producir tipos de Rust a partir de JSON. Depende de dónde vive el JSON y en qué dirección estás convirtiendo.

EnfoqueIdeal paraNota
Convertidor en línea (esta herramienta, transform.tools)Conversiones puntuales, payloads sensibles, cero instalaciónInfiere a partir de una muestra, por completo del lado del cliente
quicktypeSalida multilenguaje, generación de código en pipelinesTambién guiado por muestras; CLI o web
typeshareCompartir tipos Rust existentes con TS, Swift, KotlinDirección opuesta: de Rust a otros lenguajes
schemafy / typifyGenerar a partir de un JSON SchemaLa entrada es un schema, no una muestra
Escribir structs a manoPayloads diminutos, aprender serdeControl total, tedioso y propenso a errores a escala

La distinción que vale la pena interiorizar: un convertidor en línea y quicktype infieren tipos a partir de una muestra de JSON, mientras que typeshare va en sentido contrario, convirtiendo tipos Rust anotados en TypeScript o Swift. schemafy y typify toman un JSON Schema como entrada, no datos de ejemplo. Si tu base de código es TypeScript en lugar de Rust, el mismo enfoque guiado por muestras aplica con el convertidor de JSON a TypeScript, y la guía de interfaces de JSON a TypeScript cubre ese lado en profundidad.

Errores comunes al generar Rust a partir de JSON

Los structs generados son un punto de partida. Fíjate en esto antes de confiar en la salida con datos reales.

  • Una sola muestra rara vez revela todas las formas. Los campos opcionales solo pueden inferirse a partir de varios elementos de un arreglo. Pega un arreglo representativo para que la inferencia de Option sea precisa en lugar de una suposición basada en un objeto afortunado.
  • Los arreglos vacíos o mixtos recurren a serde_json::Value. Esa es la respuesta honesta cuando no hay nada de qué inferir. Alimenta una muestra más rica para obtener un tipo de elemento concreto.
  • No estreches un ID grande a i32. Los IDs estilo snowflake y similares superan 2^53 y desbordan un campo de 32 bits. Conserva el i64 o u64 generado.
  • Un campo siempre null se convierte en Option<serde_json::Value>, no en un opcional simple. La herramienta no tiene tipo que inferir, así que trátalo como marcador de posición y dale un tipo real en cuanto conozcas uno.
  • serde_json::Value necesita la dependencia serde_json. Si la salida contiene Value y a tu Cargo.toml le falta el crate, el código no compilará.
  • Una fecha dejada como String no hará aritmética de fechas. Si planeas comparar o formatear marcas de tiempo, cambia a un tipo de chrono en lugar de parsear cadenas a mano.

Preguntas frecuentes

¿Por qué serde rechaza mi JSON incluso cuando el struct parece correcto?

Casi siempre es una discrepancia de tipos. Las dos causas habituales son un valor de punto flotante que aterriza en un campo i64, y una clave que a veces está ausente pero no está tipada como Option. serde impone el tipo en la deserialización, así que corrige el campo a f64 o Option<T>, o regenera el struct a partir de una muestra representativa.

¿En qué tipo numérico de Rust debería convertirse un entero JSON?

Por defecto i64. Asciende a u64 cuando un valor supera i64::MAX, y recurre a f64 más allá de u64::MAX. Cualquier número escrito con punto decimal o exponente se mapea a f64 sin importar su tamaño, porque serde rechaza un float deserializado en un campo entero.

¿Cómo hago que un campo sea opcional cuando JSON lo omite?

Tipálo como Option<T>. serde trata Option como opcional automáticamente y deserializa una clave ausente a None, así que no necesitas #[serde(default)]. El convertidor marca un campo como Option cuando a algunos elementos de la muestra les falta la clave.

¿Debería usar #[serde(rename)] o #[serde(rename_all)]?

Usa #[serde(rename)] por campo cuando un payload mezcla estilos de nombres; siempre funciona. Si todos los campos de un struct siguen una convención, elimina los atributos por campo y pon en su lugar un único #[serde(rename_all = "camelCase")] en el struct. Ambos deserializan de forma idéntica.

¿Cómo manejo claves JSON que son palabras reservadas de Rust como type?

El generador emite type_ con un #[serde(rename = "type")] que mapea de vuelta a la clave original. Eso es más robusto que un identificador crudo como r#type, porque los identificadores crudos no pueden cubrir self, crate y super en toda posición.

¿Por qué las fechas JSON se tipan como String en lugar de un tipo de fecha?

JSON no tiene tipo de fecha, así que una marca de tiempo es solo una cadena en el cable, y String es el valor por defecto honesto. Para un manejo real de fechas, cambia el campo a DateTime<Utc> o NaiveDate de chrono y habilita la feature serde de chrono; entonces serde parsea RFC 3339 por ti.

¿Cómo deserializo JSON en el struct generado?

Agrega serde_json a Cargo.toml, luego escribe let root: Root = serde_json::from_str(json)?;. El derive Deserialize se encarga del resto. Usa from_slice para un slice de bytes y from_reader para un archivo o cuerpo HTTP.

¿Es privado mi JSON al usar un convertidor de JSON a struct de Rust en línea?

Sí. La conversión se ejecuta 100% en tu navegador con JavaScript. Tu JSON, incluidos tokens, IDs y datos de clientes, nunca sale de la página y nunca se envía a un servidor. Cuando estés listo, prueba el convertidor de JSON a struct de Rust con tu propio payload.

Etiquetas: rust json serde type-safety developer-tools

Artículos relacionados

Ver todos los artículos