In Toast, we have an internal server on a domain socket that accepts JSON and converts that JSON to Rust structs. One of the structs looks like this, with an enum
for the component
and wrapper
fields.
pub enum ModuleSpec {File {path: PathBuf,},Source {code: String,},}pub struct SetDataForSlug {slug: String,component: Option<ModuleSpec>,data: Option<serde_json::Value>,wrapper: Option<ModuleSpec>,}
There are a few ways to represent something like this in JSON.
serde
in this case) can figure out which type it should be based on the available fields.There are roughly three ways to tag types: externally, internally, and adjacent.
An externally tagged type uses a value outside of the content to tell the parser what type the content is. In this case we use the key of an object, where the value is the type.
[{"File": {"path": "/something.js"}},{"Source": {"code": "..."}}]
Internally tagging types places a field that explicitly tells us which type it is inside the object with the other fields. In this case, we use a type
field.
[{"type": "file","path": "/something.js"},{"type": "source","code": "..."}]
Adjacently tagged types have separate fields for the type and the content next to each other. In this case we have a type
field next to a content
field.
[{"type": "file","content": {"path": "/something.js"}},{"type": "source","content": {"code": "..."}}]
We went with internally tagged types for Toast because they are a bit easier to write and understand for the people who will be writing them (JS devs). We also chose to overload a value
field so that the name of the field is always the same, yielding one less thing to remember when swapping back and forth between the two mode
s. Example JSON payload looks like this:
{"slug": "/something","component": {"mode": "source","value": "import { h } from 'preact'; export default props => <div>hi</div>"},"data": {"some": "thing"},"wrapper": {"mode": "filepath","value": "./some/where.js"}}
The Rust code using Serde requires us to make use of both field-level attributes for renaming and container-level attributes for specifying the internal tag field.
There are also a couple tests here. This example compiles and you can check it out on the Rust playground if you want.
use serde::{Deserialize, Serialize};use std::path::PathBuf;#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]#[serde(tag = "mode")]pub enum ModuleSpec {#[serde(rename = "filepath")]File {#[serde(rename = "value")]path: PathBuf,},#[serde(rename = "source")]Source {#[serde(rename = "value")]code: String,},}#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]pub struct SetDataForSlug {/// /some/url or some/urlpub slug: String,pub component: Option<ModuleSpec>,pub data: Option<serde_json::Value>,pub wrapper: Option<ModuleSpec>,}#[cfg(test)]mod tests {use super::*;use serde_json::{json, Result, Value};#[test]fn test_deserialize_all() -> Result<()> {let data = r#"{"slug": "/something","component": {"mode": "source","value": "import { h } from 'preact'; export default props => <div>hi</div>"},"data": {"some": "thing"},"wrapper": {"mode": "filepath","value": "./some/where.js"}}"#;// Parse the string of data into serde_json::Value.let v: Value = serde_json::from_str(data)?;// Access parts of the data by indexing with square brackets.let u: SetDataForSlug = serde_json::from_value(v).unwrap();assert_eq!(SetDataForSlug {slug: String::from("/something"),component: Some(ModuleSpec::Source {code: String::from("import { h } from 'preact'; export default props => <div>hi</div>")}),data: Some(json!({"some": "thing"})),wrapper: Some(ModuleSpec::File {path: [".", "some", "where.js"].iter().collect::<PathBuf>()})},u);Ok(())}#[test]fn test_deserialize_without_data_and_wrapper() -> Result<()> {let data = r#"{"slug": "/something","component": {"mode": "source","value": "import { h } from 'preact'; export default props => <div>hi</div>"}}"#;// Parse the string of data into serde_json::Value.let v: Value = serde_json::from_str(data)?;// Access parts of the data by indexing with square brackets.let u: SetDataForSlug = serde_json::from_value(v).unwrap();assert_eq!(SetDataForSlug {slug: String::from("/something"),component: Some(ModuleSpec::Source {code: String::from("import { h } from 'preact'; export default props => <div>hi</div>")}),data: None,wrapper: None},u);Ok(())}}