🦀

Rust 小ネタ: serde_dynamo で `#[serde(from = "&str")]` が動かなくてびっくりした。

に公開

こんにちは。Fairy Devices株式会社 となんらかの関わりがある nogiro (Twitter (現 Twitter): @nogiro_iota) です。

Amazon DynamoDB と Rust プログラムのやり取りのために serde_dynamo でシリアライズ、デシリアライズしているのですが、#[serde(from = "&str")] を使うとデシリアライズでエラーが発生します。

といってもこの記事にはタイトル以上の情報はあんまりないです。#[serde(from = "String")] にしたら動きます。ソースコードはいちおう読みました。

前置き

Rust プログラムでシリアライズ、デシリアライズするときに、文字列が「種別」を表すなどの場合、それらを enum のヴァリアントで表すと便利な場合があります。例えば、後で match で分岐したりなどに利用できます。

また、想定していない文字列もいったんログに出しておきたいから受け取りはしたい、という場合があります。そういうとき、以下のように enum にその他の値を受け取るヴァリアントを用意する実装をすることがあります。

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
enum E {
    ValueA,        // `"value-a"`
    ValueB,        // `"value-b"`
    Other(String), // その他の文字列が渡された場合
}

この実装は便利ではあるのですが、"value-a" などの文字列を const として宣言しておいて別の場所でも利用したいときに不便です。(#[serde(rename = "...")] が不便という話じゃないか?)

そういう const で宣言して共通化などをしたい場合、serde の Container 属性マクロの frominto を使うと良いかもしれません。デシリアライズしてから型に impl されている From トレイトを使ってくれたり、Into トレイトを使ってからシリアライズしてくれます。自分で実装してるので当然 const なども利用できます [1]。以下が実装例です。

// それぞれに対応する文字列を const で用意できる。
const VALUE_A: &str = "value-a";
const VALUE_B: &str = "value-b";

// serde の Container attributes で from、into を指定する。
// (into は Other の中身を取り出したいので String にする。)
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(from = "&str", into = "String")]
enum E {
    ValueA,
    ValueB,
    Other(String),
}

// デシリアライズするときに &str から From トレイトを経由する。
impl From<&str> for E {
    fn from(s: &str) -> Self {
        match s {
            VALUE_A => E::ValueA,
            VALUE_B => E::ValueB,
            s => E::Other(s.into()),
        }
    }
}

// シリアライズするときに String から Into トレイトを経由する。
impl From<E> for String {
    fn from(e: E) -> Self {
        match e {
            E::ValueA => VALUE_A.to_owned(),
            E::ValueB => VALUE_B.to_owned(),
            E::Other(e) => e,
        }
    }
}

この実装は serde_json ではうまく動きます。コメントに出力される内容を書いています。

fn main() {
    let ser = serde_json::to_string(&E::ValueA).unwrap();
    println!("serialize by serde_json::to_string(): {ser:?}");
    // -> serialize by serde_json::to_string(): "\"value-a\""

    let de: E = serde_json::from_str(&ser).unwrap();
    println!("deserialize by serde_json::from_str(): {de:?}");
    // -> deserialize by serde_json::from_str(): ValueA
}

しかし、serde_dynamo を利用して同様のことをすると Err(_) が返ります。

fn main() {
    let ser: serde_dynamo::AttributeValue = serde_dynamo::to_attribute_value(&E::ValueA).unwrap();
    println!("serialize by serde_dynamo::to_attribute_value(): {ser:?}");
    // -> serialize by serde_dynamo::to_attribute_value(): S("value-a")

    let de: E = serde_dynamo::from_attribute_value(ser).unwrap();
    // -> thread 'main' panicked at src/main.rs:47:57:
    // -> called `Result::unwrap()` on an `Err` value: Error(Message("invalid type: string \"value-a\", expected a borrowed string"))
    println!("deserialize by serde_dynamo::from_attribute_value(): {de:?}");
}
ここまでの全体のソースコード
src/main.rs
use serde::{Deserialize, Serialize};

const VALUE_A: &str = "value-a";
const VALUE_B: &str = "value-b";

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(from = "&str", into = "String")]
enum E {
    ValueA,
    ValueB,
    Other(String),
}

impl From<&str> for E {
    fn from(s: &str) -> Self {
        match s {
            VALUE_A => E::ValueA,
            VALUE_B => E::ValueB,
            s => E::Other(s.to_owned()),
        }
    }
}

impl From<E> for String {
    fn from(e: E) -> Self {
        match e {
            E::ValueA => VALUE_A.to_owned(),
            E::ValueB => VALUE_B.to_owned(),
            E::Other(s) => s,
        }
    }
}

fn main() {
    let ser = serde_json::to_string(&E::ValueA).unwrap();
    println!("serialize by serde_json::to_string(): {ser:?}");
    // -> serialize by serde_json::to_string(): "\"value-a\""

    let de: E = serde_json::from_str(&ser).unwrap();
    println!("deserialize by serde_json::from_str(): {de:?}");
    // -> deserialize by serde_json::from_str(): ValueA

    let ser: serde_dynamo::AttributeValue = serde_dynamo::to_attribute_value(&E::ValueA).unwrap();
    println!("serialize by serde_dynamo::to_attribute_value(): {ser:?}");
    // -> serialize by serde_dynamo::to_attribute_value(): S("value-a")

    let de: E = serde_dynamo::from_attribute_value(ser).unwrap();
    // -> thread 'main' panicked at src/main.rs:47:57:
    // -> called `Result::unwrap()` on an `Err` value: Error(Message("invalid type: string \"value-a\", expected a borrowed string"))
    println!("deserialize by serde_dynamo::from_attribute_value(): {de:?}");
}

解決方法

とりあえず From<String> を impl して #[serde(from = "String")] を使うようにすると通ります。

use serde::{Deserialize, Serialize};

const VALUE_A: &str = "value-a";
const VALUE_B: &str = "value-b";

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(from = "String", into = "String")]
enum E {
    ValueA,
    ValueB,
    Other(String),
}

impl From<String> for E {
    fn from(s: String) -> Self {
        match s.as_str() {
            VALUE_A => E::ValueA,
            VALUE_B => E::ValueB,
            _ => E::Other(s),
        }
    }
}

// impl From<E> for String は省略

fn main() {
    let ser: serde_dynamo::AttributeValue = serde_dynamo::to_attribute_value(&E::ValueA).unwrap();
    println!("serialize by serde_dynamo::to_attribute_value(): {ser:?}");
    // -> serialize by serde_dynamo::to_attribute_value(): S("value-a")

    let de: E = serde_dynamo::from_attribute_value(ser).unwrap();
    println!("deserialize by serde_dynamo::from_attribute_value(): {de:?}");
    // -> deserialize by serde_dynamo::from_attribute_value(): ValueA
}

なぜなのか?

ローカルにソースコードを持ってきて、

git clone git@github.com:zenlist/serde_dynamo.git crates/serde_dynamo/
git clone git@github.com:serde-rs/json.git crates/serde_json/

依存先を変えて、

Cargo.toml
-serde_dynamo = { version = "4.2.14", default-features = false }
-serde_json = { version = "1.0.142", default-features = false, features = ["std"] }
+serde_dynamo = { path = "crates/serde_dynamo/" }
+serde_json = { path = "crates/serde_json/", features = ["std"] }

print デバッグしてみたところ、直接的には この行visitor.visit_string() を呼んでいるために「a borrowed string」ではないとして Err(_) を返しているようです。

もう少し具体的には、from 属性マクロ によって

<T>::deserialize(deserializer).map(From::from)

に展開されてから [2]ここT&str であることから StrVisitorvisit_str() が呼ばれるけど、StrVisitor には visit_str() が実装されていないため、Visitor のデフォルト実装Err(_) を返しています。「a borrowed string」という文字列も StrVisitor返しており、エラー文は ここ で構築されています。

本質的には、この現象は serde_dynamo::from_attribute_value()&serde_dynamo::AttributeValue ではなく serde_dynamo::AttributeValue を受け取っていることで起こっているのだと思います。

JSON 文字列などからデシリアライズするのと異なり、serde_dynamo::AttributeValue::S は内部に String を持っています。その String をむやみにコピーしないようムーブしているためと言い換えることもできます。いろいろ考えられていますね。[3]

終わり

serde のソースコードをいっぱい読んで楽しかった。

脚注
  1. もちろん自分で DeserializeSerialize を impl するでも良いです。 ↩︎

  2. From::fromlet _i: Result<i64, ()> = Result::Ok(3i32).map(From::from); // -> "Ok(3)" みたいに型推論してくれて便利。 ↩︎

  3. いろいろと考えてくれていますが、into を使うと Clone される ので台無しである。 ↩︎

フェアリーデバイセズ公式

Discussion