Rust中JSON如何解析转换到struct

写Rust有一年多了,我是非常喜欢严谨的语言,就连 Rust 的Logo看上去都很工业化,具有严谨性。我愿意付出相对多写代码的成本去换取更稳定,更少 Runtime 错误。写代码到今天已经有将近十年了,一直想选一款编译型语言,之前有用过Golang,但仍然不太满意的是编译后的语言仍然在运行时出现空指针错误,而Rust是我见过跟众多语言别具一格的做法,没有null类型。是的,你没有看错,在Rust里没有null类型。加之在Rust编译器严谨的模式下,你很难写出在Runtime时有空指针的错误。

今天我们就来说说一个常规性的操作,解析JSON内容,并转换到struct,我们主要使用 serde_json 这个包。

添加依赖

我们主要是用 serde_json 这个依赖包,首先在 Cargo.toml 里添加 dependencies :

Cargo.toml

[dependencies]
serde_json = "1.0"

Rust推荐使用 Semantic Versioning 命名版本号号version,格式如下

MAJOR.MINOR.PATCH

按照我的经验,我推荐所有添加到依赖 dependencies 里的所有依赖包version部分省去末尾 PATCH 比较好,因为 MAJORMINOR 固定之后,即使升级到最新 PATCH 一般不会出现问题,因为 PATCH 是用来修复Bug,以及其它一些不太影响程序主体的变动。在 Cargo.toml 里,当你仅仅声明版本号是 1.0 时, PATCH 部分会自动获取最新版本号。这也就是为什么在上面的例子中我写了 1.0 做为 serde_json 依赖版本号

JSON转Value枚举值

接下来,我们就可以使用 serde_json 的API去解析JSON了,如果你很懒,不想定义struct, serde_json 也提供了一个枚举值 Value ,它枚举出了所有JSON里可能出现的数据类型,包括null,如:

main.rs

use serde_json::{Result, Value};

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

  let json = r#"
  {
    "name": "Nicholas Lee",
    "age": 30,
    "blog": "https://www.qttc.net",
    "addr": null
  }"#;

  let v: Value = serde_json::from_str(json)?;

  println!("name = {}", v["name"]);
  println!("age = {}", v["age"]);
  println!("blog = {}", v["blog"]);
  println!("addr = {}", v["addr"]);

  Ok(())
}

Output

$ cargo run
   Compiling simple-json v0.1.0 (/Users/nicholas/rust/simple-json)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/simple-json`
name = "Nicholas Lee"
age = 30
blog = "https://www.qttc.net"
addr = null

JSON转Struct

通常,我是尽量使用struct接收转换结果,这样在后续使用时可以减少运行时的错误,在编译阶段能发现低级错误。我们首先要在 Cargo.toml 里添加 serde 依赖包,它主要专门处理序列化与反序列化的。

Cargo.toml

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

按照以上的例子,我们先定义一个名为User的struct用来接收结果,大概如下

struct User {
  name: String,
  age: u8,
  blog: String,
  addr: String,
}

这里的 age 我用 u8 类型,因为描述一个人的年龄使用一个字节就足够了,Rust没有 null 类型,所以上面例子中 addr 字段必须把 null 去掉改成一个字符串,否则会报错,最终完整代码如下

main.rs

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

#[derive(Serialize, Deserialize)]
struct User {
  name: String,
  age: u8,
  blog: String,
  addr: String,
}

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

  let json = r#"
  {
    "name": "Nicholas Lee",
    "age": 30,
    "blog": "https://www.qttc.net",
    "addr": "4114 Sepulveda Blvd"
  }"#;

  let u: User = serde_json::from_str(json)?;

  println!("name = {}", u.name);
  println!("age = {}", u.age);
  println!("blog = {}", u.blog);
  println!("addr = {}", u.addr);

  Ok(())
}

Output

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/simple-json`
name = Nicholas Lee
age = 30
blog = https://www.qttc.net
addr = 4114 Sepulveda Blvd

JSON字段名与Struct不匹配怎么办

Rust提倡使用 Snake-Case 蛇式方式命名变量,但有时候JSON的个别字段是 Camel-Case 驼峰式命名,如:

user.json

{
  "fullName": "Nicholas Lee",
  "age": 30,
  "blog": "https://www.qttc.net",
  "addr": "4114 Sepulveda Blvd"
}

上面的JSON数据里,其中 fullNameCamel-Case 驼峰式命名,那么你的struct里也要有一个与它同名的字段才能解析转换,如

struct User {
  fullName: String,
  age: u8,
  blog: String,
  addr: String,
}

此时,Rust会给你一个告警

structure field `fullName` should have a snake case name

note: `#[warn(non_snake_case)]` on by default
help: convert the identifier to snake case: `full_name`rustc(non_snake_case)

提示你使用 Snake-Case 蛇式命名struct里的 fullName 字段,或者通过 #[warn(non_snake_case)] 把告警关了。虽然告警不影响程序编译运行,但我通常不喜欢改掉默认规则,于是还有另外一种方法完美解决这个问题,使用 rename 特性,如

struct User {
  #[serde(rename = "fullName")]
  full_name: String,
  age: u8,
  blog: String,
  addr: String,
}

rename = "fullName" 表示从JSON中读取 fullName 字段。最后完整的例子如下

main.rs

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

#[derive(Serialize, Deserialize)]
struct User {
  #[serde(rename = "fullName")]
  full_name: String,
  age: u8,
  blog: String,
  addr: String,
}

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

  let json = r#"
  {
    "fullName": "Nicholas Lee",
    "age": 30,
    "blog": "https://www.qttc.net",
    "addr": "4114 Sepulveda Blvd"
  }"#;

  let u: User = serde_json::from_str(json)?;

  println!("full name = {}", u.full_name);
  println!("age = {}", u.age);
  println!("blog = {}", u.blog);
  println!("addr = {}", u.addr);

  Ok(())
}

Output

$ cargo run
   Compiling simple-json v0.1.0 (/Users/nicholas/rust/simple-json)
    Finished dev [unoptimized + debuginfo] target(s) in 0.81s
     Running `target/debug/simple-json`
full name = Nicholas Lee
age = 30
blog = https://www.qttc.net
addr = 4114 Sepulveda Blvd

null处理

如果用struct接收JSON转换结果,就自然免不了要处理 null 的情况,所以我们要把有可能出现 null 的字段使用Rust自带的枚举 Option<T> 包裹起来。如

user.json

{
  "full_name": "Nicholas Lee",
  "age": 30,
  "blog": "https://www.qttc.net",
  "addr": null
}

以上JSON数据里的 addr 字段为 null ,如果你仍然使用 addr: String 来接收,那么在编译时获得一个错误

Error: Error("invalid type: null, expected a string", line: 6, column: 16)

上面的错误提示类型无效,所以我们的struct也要做相应修改,改用 Option<String> 处理 null

struct User {
  full_name: String,
  age: u8,
  blog: String,
  addr: Option<String>,
}

最终代码如下

main.rs

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

#[derive(Serialize, Deserialize)]
struct User {
  full_name: String,
  age: u8,
  blog: String,
  addr: Option<String>,
}

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

  let json = r#"
  {
    "full_name": "Nicholas Lee",
    "age": 30,
    "blog": "https://www.qttc.net",
    "addr": null
  }"#;

  let u: User = serde_json::from_str(json)?;

  println!("full name = {}", u.full_name);
  println!("age = {}", u.age);
  println!("blog = {}", u.blog);
  println!("addr = {}", u.addr.unwrap_or("null".to_string()));

  Ok(())
}

Output

$ cargo run
   Compiling simple-json v0.1.0 (/Users/nicholas/rust/simple-json)
    Finished dev [unoptimized + debuginfo] target(s) in 0.72s
     Running `target/debug/simple-json`
full name = Nicholas Lee
age = 30
blog = https://www.qttc.net
addr = null

null 值的处理搞定

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章