はじめ
DynamoDBにて、JSON文字列をそのままString型で格納するケースで(設計の是非はさておき)、serde_dynamoではString型のフィールドにJSONが入っている場合、そのままではネストしたStructへのDeserializeができないなーと思いました
LLMは自分でDeserializerを作れやと言ってきましたが、作りたくありませんでした
前提
DynamoDBのデータ構造
以下のようなアイテムを想定
{
"id": { "S": "hoge1234" },
"name": { "S": "huga" },
"details": { "S": "{\"key\":\"hoge\",\"description\":\"hogeeeeeeee\"}" }
}
details フィールドはDynamo上ではStringだが、中にはJSONがぶち込まれている
作りたいRustのStruct
#[derive(Debug, Deserialize)]
pub struct Hoge {
pub id: String,
pub name: String,
pub details: HogeDetails, // ここをいいかんじにしたい
}
#[derive(Debug, Deserialize)]
pub struct HogeDetails {
pub key: String,
pub description: String,
}
serde_dynamoだけでは動かない
※Dynamoから取得する部分のソースなどはネットの海に腐るほどあるので省略
Error: ErrorImpl { code: Message("invalid type: string \"...\", expected a map"), ... }
serde_dynamo は details フィールドを AttributeValue::S として受け取る
しかしRust側は HogeDetails Struct(Map)を期待している。String をそのままMapにキャストしようとしてエラー
serde_dynamo はDynamoの型システム(AttributeValue)を意識したものであり、Stringの中にさらに別のシリアライズ形式があることはさすがに解釈してくれない
serde_withのJsonStringを使えばいけた
serde_withが提供する JsonStringを使う
文字列として受け取った値を、さらにJSONとしてDeserializeするようにすればいいらしい
依存関係を追加
# Cargo.toml
[dependencies]
serde_with = { version = "3.16.1", features = ["json"] }
versionだけでなくfeatures = ["json"] も追加
Struct側に追記
use serde::Deserialize;
use serde_with::{serde_as, json::JsonString};
#[serde_as]
#[derive(Debug, Deserialize)]
pub struct Hoge {
pub id: String,
pub name: String,
#[serde_as(as = "JsonString")] // 追加
pub details: HogeDetails,
}
#[derive(Debug, Deserialize)]
pub struct HogeDetails {
pub key: String,
pub description: String,
}
#[serde_as] をStructの先頭に、対象フィールドに #[serde_as(as = "JsonString")] を付けて誘導すればよい
serde_with の #[serde_as(as = "...")] は、フィールドごとにDeserialize方針を切り替えられるよう
他にもいろいろあるらしいが割愛
おわり
DB側を素直にMapに設計したほうがいいとおもいました
(空でソースを書いているので間違ってたらすみません)