Rust中JSON如何解析转换到struct

rust json

写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": "琼台博客",
    "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 = "琼台博客"
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": "琼台博客",
    "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 = 琼台博客
age = 30
blog = https://www.qttc.net
addr = 4114 Sepulveda Blvd

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

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

user.json

{
  "fullName": "琼台博客",
  "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": "琼台博客",
    "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 = 琼台博客
age = 30
blog = https://www.qttc.net
addr = 4114 Sepulveda Blvd

null处理

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

user.json

{
  "full_name": "琼台博客",
  "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": "琼台博客",
    "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 = 琼台博客
age = 30
blog = https://www.qttc.net
addr = null

null值的处理搞定

分享

TITLE: Rust中JSON如何解析转换到struct

LINK: https://www.qttc.net/509-rust-parse-json.html

NOTE: 原创内容,转载请注明出自琼台博客