Repository: mitsuhiko/insta
Pull request #255 (2022-07-31 04:25:37)

Initial changes to drop default serde dependency
Related #214 

## Motivation

This was an experiment to see how difficult it would be to drop `serde` usage by default (including `serde_json` and `serde_yaml`). The idea was to see how lightweight `insta` could be in both clean compile times and the number of dependencies if someone was just using the debug, display, and string snapshot formats

## (Spoilers) :boom: Breaking Changes :boom:

1. This does involve removing the `Deserialie` and `Serialize` trait from several publicly exposed types
2. The type for `info` got changed to a `String` although I think it could just be moved behind the `serialization` feature flag
3. The `assert_yaml_snapshot!()` and `assert_json_snapshot!()` formats got moved behind the `yaml` and `json` feature flags respectively

## Implementation

The `serde` implementation was replaced by a handwritten baby version that somewhat follows the same principle. The idea is still to translate the underlying data format to a common structure (in this case `crate::parse::Value`) and to have logic to serialize and deserialize data for `struct`s with that common format

To make the implementation simple the common format is kept very basic to make it easy to map to the data returned by the parsers (This can be seen in `src/parse/mod.rs`). Also the mapping of the common data format to different structs had to be handwritten instead of just being able to use `#[derive(Deserialize, Serialize)]` (This can be seen in `src/snapshot.rs`)

## Results

_Big dislaimer: The main benefit is to people who can get by with just the debug, display, and string snapshot formats_

This drastically drops the number of default dependencies

```console
$ cargo tree --edges no-dev
insta v1.17.1 (/home/wintermute/Programming/Repos/insta)
├── console v0.15.1
│   ├── libc v0.2.126
│   ├── once_cell v1.11.0
│   └── terminal_size v0.1.17
│       └── libc v0.2.126
├── json v0.12.4
├── linked-hash-map v0.5.6
├── once_cell v1.11.0
├── similar v2.1.0
│   ├── bstr v0.2.17
│   │   ├── lazy_static v1.4.0
│   │   ├── memchr v2.5.0
│   │   └── regex-automata v0.1.10
│   └── unicode-segmentation v1.9.0
└── yaml-rust v0.4.5
    └── linked-hash-map v0.5.6
```

Along with having a large impact on clean build times (Note _before_ filters out crates that built in less than 0.3 seconds while _after_ filters out less than 0.2s)

_Before_

![baseline](https://user-images.githubusercontent.com/30302768/182009751-d414e18c-2771-475c-b70d-25ea909f1616.png)

_After_

![slim](https://user-images.githubusercontent.com/30302768/182009765-fc10eef5-a100-4bb1-846c-bba2b875735e.png)

I also took a brief survey over 20 highly downloaded crates that use `insta` to see how many use formats outside of debug, default, and string. From

```
cargo-emit	    cynic	   hostlist-parser   opentelemetry-application-insights  symbolic-debuginfo
cargo-watch	    cynic-codegen  html_parser	     paperclip				 symbolic-symcache
cargo-workspaces    delog	   lustre_collector  similar				 syn
codespan-reporting  full_moon	   minijinja	     stylua				 test-case
```

only `cynic`, `full_moon`, and `html_parser` used a format outside of debug, default, and string. `yaml` for `cynic` and `full_moon` and `json` for `html_parser`

## Pending Work

_I didn't want to take the time to polish everything since I'm not sure this will even be merged considering it involves breaking changes_

- Going through and updating docs
- Probably switching `info` back to `Serialize` probably

mitsuhiko: I like this a lot. There are even quite quite significant benefits for people who are using the serde formats with this. However what I'm not a fan of is that the snapshot format no longer carries info directly and expression is missing entirely:

Before:

```
---
source: tests/test_settings.rs
description: The snapshot are four integers
expression: "vec![1, 2, 3, 4]"
info:
  env:
    ENVIRONMENT: production
  cmdline:
    - my-tool
    - run
---
```

After:

```
---
source: tests/test_settings.rs
description: The snapshot is four integers
info: "env: [\"ENVIRONMENT\", \"production\"], cmdline: [\"my-tool\", \"run\"]"
---
```

I generally think that the info change to a string would be something I would not be okay with. Since apparently `yaml` is a dependency now anyways it should be easy enough to parse this. The info object can also be represented by the internal `Value` type rather than the serde_yaml Value type instead.

More importantly in general `content::Content` and `parse::Value` are more or less duplicates. I think it would be a better call to make them the same type and only add the serde specific trait implementations for the former as needed. There is no need to carry two value types in the crate for what is effectively the same purpose. The many different types on the content type could be phased out, they are not that useful and i believe they are already invisible to observers.

I also generally believe that `serde_json` and `serde_yaml` could be entirely pointless as dependencies. For as long as `Content`/`Value` implements optionally `serde::Serialize` it would be possible to make a minimal adapter that just serializes out via `yaml`/`json`.

In the interest of cutting out more dependencies the `json` dependency as it exists at the moment is completely unneeded. The only use of parsing seems to be the cargo manifest which can be parsed with the yaml crate as well.

Good work overall. I think this can turn into something quite valuable!
mitsuhiko: I pushed a small fix for `expression` up.
LovecraftianHorror: I totally agree with all the feedback. A brief summary:

- Drop `parse::Value` in favor of `content::Content`
- Switch `info` to store a `Value`/`Content` and potentially expose two apis to set it (one by giving the raw value and the other taking some `Serialize` type)
- Potentially drop `serde_json` and `serde_yaml` entirely by substituting `Content` rendering to `json`/`yaml`

The one thing I still have questions on is

> In the interest of cutting out more dependencies the `json` dependency as it exists at the moment is completely unneeded. The only use of parsing seems to be the cargo manifest which can be parsed with the yaml crate as well

I'm assuming that you're mixing up YAML and TOML when it comes to parsing the manifests unless there's something else I'm missing

Also worth noting that `json` is currently being used to create the pending inline snapshot files which seems to be a line delimited JSON format. I wonder if we could store them as a YAML array instead. It should be possible to make `cargo-insta` handle both formats to handle backwards compatibility

---

> Good work overall. I think this can turn into something quite valuable! Oh in terms of if this could be merged: absolutely. Mostly because I believe this could even be smooth by going in two version steps:
> - In the first release: a new _hidden_serde feature is added which is on by default which gates Serialize and Deserialize on public traits along with the serde based assertion macros and gives a deprecation warning unless the necessary yaml and json features are also enabled.
> - Next release that is off by default.

Awesome! That's great to hear

I'll work towards addressing all the feedback above, and thanks for fixing `expression` being missing. That change was unintentional :sweat_smile: 
mitsuhiko: > I'm assuming that you're mixing up YAML and TOML when it comes to parsing the manifests unless there's something else I'm missing

YAML is a superset of JSON so for the purpose of reading cargo manifest files you can use the yaml-rust crate:

```rust
use std::process::Command;

fn main() {
    let out = Command::new("cargo")
        .arg("read-manifest")
        .output()
        .unwrap();
    let manifest = yaml_rust::YamlLoader::load_from_str(
        std::str::from_utf8(&out.stdout).unwrap()).unwrap();
    dbg!(manifest);
}
```

About this in particular:

> Also worth noting that json is currently being used to create the pending inline snapshot files which seems to be a line delimited JSON format. I wonder if we could store them as a YAML array instead. It should be possible to make cargo-insta handle both formats to handle backwards compatibility

For now I would probably keep the logic here but since insta (not cargo insta) only needs to emit these files I wouldn't be entirely opposed to a minimal JSON emitter which can be used for snapshots and this. #256 already showed that relying on generic serialization libraries is not great for insta as formats change all the time with updates, so actually having an embedded JSON emitter in insta is something I would probably prefer having _anyways_. But in the interest of keeping the PR mergeable I think that should be a followup task. So I would keep the json dependency exclusively for the inline snapshots pending file for now, and I will then follow up with something that kills that too.
LovecraftianHorror: Pushed changes that:

- Replace usages of `parse::Value` with `content::Content`
- Make `info` take `Serialize` and add in a `raw_info` that takes a `content::Content` e.g.

```rust
use insta::internals::Content;

let raw_info = Content::Map(vec![
    (
        Content::from("env"),
        Content::Seq(vec![
            Content::from("ENVIRONMENT"),
            Content::from("production"),
        ]),
    ),
    (
        Content::from("cmdline"),
        Content::Seq(vec![Content::from("my-tool"), Content::from("run")]),
    ),
]);
with_settings!({description => "The snapshot is four integers", raw_info => &raw_info}, {
    assert_debug_snapshot!(vec![1, 2, 3, 4])
});
```

- Lower the MSRV to 1.51.0 when using default features

---

I think this is a good stopping point for this PR to avoid scope creep and trying out a bunch of changes that may need hashing out. With that said the pending work left is (which I'm certainly happy to work on, but feel free to pick some if you want)

- [ ] Switch cargo manifest parsing and pending inline snapshots to use YAML dropping the need for `json`
    - This will involve changes in `cargo-insta` to support both formats for backwards compat.
- [ ] Evaluate replacing `serde_yaml` with `Content::as_yaml` (manually verify with several projects to make sure there aren't any differences)
- [ ] Smooth the breaking changes with:

> In the first release: a new `_hidden_serde` feature is added which is on by default which gates `Serialize` and `Deserialize` on public traits along with the serde based assertion macros and gives a deprecation warning unless the necessary `yaml` and `json` features are also enabled.

- [ ] Deprecate the `backtrace` feature now that it's no longer used? (seems like we might as well)
- [ ] Update docs to reflect all the changes
mitsuhiko: This looks very good. I think I will be merging this to master and then look from there how to close on the remaining items.

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 623215a..a25d5bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ default = ["colors"]
 
 # when the redactions feature is enabled values can be redacted in serialized
 # snapshots.
-redactions = ["pest", "pest_derive"]
+redactions = ["pest", "pest_derive", "serialization"]
 
 # Enables support for running filters on snapshot
 filters = ["regex"]
@@ -34,26 +34,39 @@ glob = ["walkdir", "globset"]
 colors = ["console"]
 
 # This feature is now just always enabled because we use yaml internally now.
-serialization = []
+serialization = ["serde"]
 
 # This feature is no longer used as snapshot name detection was changed.
 backtrace = []
 
+# Serialization formats
+# TODO: This could be cleaner by using "dep:csv" without renaming the dep, but
+# this technique allows for a lower MSRV
+csv = ["dep_csv", "serialization"]
+json = ["serde_json", "serialization"]
+ron = ["dep_ron", "serialization"]
+toml = ["dep_toml", "serialization"]
+yaml = ["serde_yaml", "serialization"]
+
 [dependencies]
-csv = { version = "1.1.4", optional = true }
-serde = { version = "1.0.117", features = ["derive"] }
-serde_yaml = "0.8.26"
+dep_csv = { package = "csv", version = "1.1.4", optional = true }
 console = { version = "0.15.1", optional = true, default-features = false }
-serde_json = "1.0.59"
 pest = { version = "2.1.3", optional = true }
 pest_derive = { version = "2.1.0", optional = true }
-ron = { version = "0.7.1", optional = true }
-toml = { version = "0.5.7", optional = true }
+dep_ron = { package = "ron", version = "0.7.1", optional = true }
+dep_toml = { package = "toml", version = "0.5.7", optional = true }
 globset = { version = "0.4.6", optional = true }
 walkdir = { version = "2.3.1", optional = true }
 similar = { version = "2.1.0", features = ["inline"] }
 once_cell = "1.9.0"
 regex = { version = "1.6.0", default-features = false, optional = true, features = ["std", "unicode"] }
+yaml-rust = "0.4.5"
+dep_json = { package = "json", version = "0.12.4" }
+serde = { version = "1.0.117", optional = true }
+serde_json = { version = "1.0.59", optional = true }
+serde_yaml = { version = "0.8.26", optional = true }
+linked-hash-map = "0.5.6"
 
 [dev-dependencies]
+serde = { version = "1.0.117", features = ["derive"] }
 similar-asserts = "1.4.2"
diff --git a/cargo-insta/Cargo.lock b/cargo-insta/Cargo.lock
index c16aad8..b35a048 100644
--- a/cargo-insta/Cargo.lock
+++ b/cargo-insta/Cargo.lock
@@ -262,6 +262,8 @@ name = "insta"
 version = "1.17.1"
 dependencies = [
  "console",
+ "json",
+ "linked-hash-map",
  "once_cell",
  "pest",
  "pest_derive",
@@ -269,6 +271,7 @@ dependencies = [
  "serde_json",
  "serde_yaml",
  "similar",
+ "yaml-rust",
 ]
 
 [[package]]
@@ -277,6 +280,12 @@ version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "json"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -291,9 +300,9 @@ checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
 
 [[package]]
 name = "linked-hash-map"
-version = "0.5.4"
+version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
 [[package]]
 name = "log"
diff --git a/cargo-insta/Cargo.toml b/cargo-insta/Cargo.toml
index 499ba13..d660194 100644
--- a/cargo-insta/Cargo.toml
+++ b/cargo-insta/Cargo.toml
@@ -12,7 +12,7 @@ edition = "2018"
 readme = "README.md"
 
 [dependencies]
-insta = { version = "=1.17.1", path = "..", features = ["redactions"] }
+insta = { version = "=1.17.1", path = "..", features = ["json", "yaml", "redactions"] }
 console = "0.15.1"
 structopt = "0.3.20"
 serde = { version = "1.0.117", features = ["derive"] }
diff --git a/src/content/error.rs b/src/content/error.rs
new file mode 100644
index 0000000..a6d63e5
--- /dev/null
+++ b/src/content/error.rs
@@ -0,0 +1,30 @@
+use std::{error, fmt, result};
+
+use super::Content;
+
+pub type Result<T> = result::Result<T, Error>;
+
+#[derive(Debug)]
+pub enum Error {
+    InvalidStructField(Content),
+    FailedParsingYaml,
+    UnexpectedDataType,
+    MissingField,
+    UnsupportedDataType,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::InvalidStructField(content) => write!(f, "Invalid struct field: {:?}", content),
+            Self::FailedParsingYaml => f.write_str("Failed parsing the provided YAML text"),
+            Self::UnexpectedDataType => {
+                f.write_str("The present data type wasn't what was expected")
+            }
+            Self::MissingField => f.write_str("A required field was missing"),
+            Self::UnsupportedDataType => f.write_str("Failed converting an unsupported data type"),
+        }
+    }
+}
+
+impl error::Error for Error {}
diff --git a/src/content/formats.rs b/src/content/formats.rs
new file mode 100644
index 0000000..f0027ef
--- /dev/null
+++ b/src/content/formats.rs
@@ -0,0 +1,260 @@
+use std::convert::TryFrom;
+
+use super::{Content, Error, Result};
+
+use dep_json::{object::Object as JsonObj, JsonValue};
+use yaml_rust::{yaml::Hash as YamlObj, Yaml as YamlValue};
+
+type BoxedResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
+
+impl Content {
+    // NOTE: Not implemented as `TryFrom<_>` because this is not generic and we want it to remain
+    // private
+    pub(crate) fn from_json(s: &str) -> BoxedResult<Self> {
+        let blob = dep_json::parse(s)?;
+        Ok(Self::from_json_blob(blob))
+    }
+
+    fn from_json_blob(blob: JsonValue) -> Self {
+        match blob {
+            JsonValue::Null => Self::None,
+            JsonValue::Boolean(b) => Self::from(b),
+            JsonValue::Number(n) => match i64::try_from(n) {
+                Ok(int) => Self::from(int),
+                Err(_) => {
+                    let f = f64::from(n);
+                    Self::from(f)
+                }
+            },
+            JsonValue::Short(s) => Self::from(s.to_string()),
+            JsonValue::String(s) => Self::from(s),
+            JsonValue::Array(seq) => {
+                let seq = seq.into_iter().map(Self::from_json_blob).collect();
+                Self::Seq(seq)
+            }
+            // This is probably a struct, but we don't have context on the struct name
+            JsonValue::Object(obj) => {
+                // TODO: why doesn't this have `.into_iter()`
+                let obj = obj
+                    .iter()
+                    .map(|(k, v)| (Self::from(k), Self::from_json_blob(v.to_owned())))
+                    .collect();
+                Self::Map(obj)
+            }
+        }
+    }
+
+    pub(crate) fn as_json(&self) -> BoxedResult<String> {
+        let json_blob = self.as_json_blob()?;
+
+        let mut buf = Vec::new();
+        json_blob.write(&mut buf).unwrap();
+
+        Ok(String::from_utf8(buf).expect("JSON is valid UTF-8"))
+    }
+
+    fn as_json_blob(&self) -> Result<JsonValue> {
+        fn translate_seq(seq: &[Content]) -> Result<JsonValue> {
+            let seq = seq
+                .iter()
+                .map(Content::as_json_blob)
+                .collect::<Result<_>>()?;
+            Ok(JsonValue::Array(seq))
+        }
+
+        fn translate_fields(fields: &[(&str, Content)]) -> Result<JsonValue> {
+            let obj = fields
+                .iter()
+                .map(|(k, v)| Ok((k.to_owned(), v.as_json_blob()?)))
+                .collect::<Result<_>>()?;
+            Ok(JsonValue::Object(obj))
+        }
+
+        match self {
+            Self::Bool(b) => Ok(JsonValue::from(*b)),
+            Self::U8(n) => Ok(JsonValue::from(*n)),
+            Self::U16(n) => Ok(JsonValue::from(*n)),
+            Self::U32(n) => Ok(JsonValue::from(*n)),
+            Self::U64(n) => Ok(JsonValue::from(*n)),
+            // TODO: switch to a parser that supports u128/i128
+            Self::U128(_) => Err(Error::UnsupportedDataType),
+            Self::I8(n) => Ok(JsonValue::from(*n)),
+            Self::I16(n) => Ok(JsonValue::from(*n)),
+            Self::I32(n) => Ok(JsonValue::from(*n)),
+            Self::I64(n) => Ok(JsonValue::from(*n)),
+            // TODO: switch to a parser that supports u128/i128
+            Self::I128(_) => Err(Error::UnsupportedDataType),
+            Self::F32(f) => Ok(JsonValue::from(*f)),
+            Self::F64(f) => Ok(JsonValue::from(*f)),
+            Self::Char(c) => Ok(JsonValue::from(c.to_string())),
+            Self::String(s) => Ok(JsonValue::from(s.as_str())),
+            Self::Bytes(bytes) => {
+                let bytes = bytes.iter().map(|b| JsonValue::from(*b)).collect();
+                Ok(JsonValue::Array(bytes))
+            }
+            Self::None | Self::Unit | Self::UnitStruct(_) => Ok(JsonValue::Null),
+            Self::Some(content) => content.as_json_blob(),
+            Self::UnitVariant(_, _, variant) => Ok(JsonValue::from(*variant)),
+            Self::NewtypeStruct(_, content) => content.as_json_blob(),
+            Self::NewtypeVariant(_, _, variant, content) => {
+                let mut obj = JsonObj::new();
+                obj.insert(variant, content.as_json_blob()?);
+                Ok(JsonValue::Object(obj))
+            }
+            Self::Seq(seq) | Self::Tuple(seq) | Self::TupleStruct(_, seq) => translate_seq(seq),
+            Self::TupleVariant(_, _, variant, seq) => {
+                let mut obj = JsonObj::new();
+                obj.insert(variant, translate_seq(seq)?);
+                Ok(JsonValue::Object(obj))
+            }
+            Self::Map(map) => {
+                let map = map
+                    .iter()
+                    .map(|(k, v)| {
+                        if let Self::String(k) = k {
+                            Ok((k, v.as_json_blob()?))
+                        } else {
+                            Err(Error::InvalidStructField(k.to_owned()))
+                        }
+                    })
+                    .collect::<Result<_>>()?;
+
+                Ok(JsonValue::Object(map))
+            }
+            Self::Struct(_, fields) => translate_fields(fields),
+            Self::StructVariant(_, _, variant, fields) => {
+                let mut obj = JsonObj::new();
+                obj.insert(variant, translate_fields(fields)?);
+                Ok(JsonValue::Object(obj))
+            }
+        }
+    }
+
+    // NOTE: Not implemented as `TryFrom<_>` because this is not generic and we want it to remain
+    // private
+    pub(crate) fn from_yaml(s: &str) -> Result<Self> {
+        let mut blobs =
+            yaml_rust::YamlLoader::load_from_str(s).map_err(|_| Error::FailedParsingYaml)?;
+
+        match (blobs.pop(), blobs.pop()) {
+            (Some(blob), None) => Self::from_yaml_blob(blob).map_err(Into::into),
+            _ => Err(Error::FailedParsingYaml),
+        }
+    }
+
+    fn from_yaml_blob(blob: YamlValue) -> Result<Self> {
+        match blob {
+            YamlValue::Null => Ok(Self::None),
+            YamlValue::Boolean(b) => Ok(Self::from(b)),
+            YamlValue::Integer(num) => Ok(Self::from(num)),
+            YamlValue::Real(real_str) => {
+                let real: f64 = real_str.parse().unwrap();
+                Ok(Self::from(real))
+            }
+            YamlValue::String(s) => Ok(Self::from(s)),
+            YamlValue::Array(seq) => {
+                let seq = seq
+                    .into_iter()
+                    .map(Self::from_yaml_blob)
+                    .collect::<Result<_>>()?;
+                Ok(Self::Seq(seq))
+            }
+            YamlValue::Hash(obj) => {
+                let obj = obj
+                    .into_iter()
+                    .map(|(k, v)| Ok((Self::from_yaml_blob(k)?, Self::from_yaml_blob(v)?)))
+                    .collect::<Result<_>>()?;
+                Ok(Self::Map(obj))
+            }
+            YamlValue::BadValue | YamlValue::Alias(_) => Err(Error::FailedParsingYaml),
+        }
+    }
+
+    pub(crate) fn as_yaml(&self) -> String {
+        let yaml_blob = self.as_yaml_blob();
+
+        let mut buf = String::new();
+        let mut emitter = yaml_rust::YamlEmitter::new(&mut buf);
+        emitter.dump(&yaml_blob).unwrap();
+
+        buf.push('\n');
+        buf
+    }
+
+    fn as_yaml_blob(&self) -> YamlValue {
+        fn translate_seq(seq: &[Content]) -> YamlValue {
+            let seq = seq.iter().map(Content::as_yaml_blob).collect();
+            YamlValue::Array(seq)
+        }
+
+        fn translate_fields(fields: &[(&str, Content)]) -> YamlValue {
+            let fields = fields
+                .iter()
+                .map(|(k, v)| (YamlValue::String(k.to_string()), v.as_yaml_blob()))
+                .collect();
+            YamlValue::Hash(fields)
+        }
+
+        match self {
+            Self::Bool(b) => YamlValue::Boolean(*b),
+            Self::U8(n) => YamlValue::Integer(i64::from(*n)),
+            Self::U16(n) => YamlValue::Integer(i64::from(*n)),
+            Self::U32(n) => YamlValue::Integer(i64::from(*n)),
+            Self::U64(n) => YamlValue::Real(n.to_string()),
+            Self::U128(n) => YamlValue::Real(n.to_string()),
+            Self::I8(n) => YamlValue::Integer(i64::from(*n)),
+            Self::I16(n) => YamlValue::Integer(i64::from(*n)),
+            Self::I32(n) => YamlValue::Integer(i64::from(*n)),
+            Self::I64(n) => YamlValue::Integer(*n),
+            Self::I128(n) => YamlValue::Real(n.to_string()),
+            Self::F32(f) => YamlValue::Real(f.to_string()),
+            Self::F64(f) => YamlValue::Real(f.to_string()),
+            Self::Char(c) => YamlValue::String(c.to_string()),
+            Self::String(s) => YamlValue::String(s.to_owned()),
+            Self::Bytes(bytes) => {
+                let bytes = bytes
+                    .iter()
+                    .map(|b| YamlValue::Integer(i64::from(*b)))
+                    .collect();
+                YamlValue::Array(bytes)
+            }
+            Self::None | Self::Unit | Self::UnitStruct(_) => YamlValue::Null,
+            Self::Some(content) => content.as_yaml_blob(),
+            Self::UnitVariant(_, _, variant) => YamlValue::String(variant.to_string()),
+            Self::NewtypeStruct(_, content) => content.as_yaml_blob(),
+            Self::NewtypeVariant(_, _, variant, content) => {
+                let mut obj = YamlObj::new();
+                obj.insert(
+                    YamlValue::String(variant.to_string()),
+                    content.as_yaml_blob(),
+                );
+                YamlValue::Hash(obj)
+            }
+            Self::Seq(seq) => translate_seq(seq),
+            Self::Tuple(seq) => translate_seq(seq),
+            Self::TupleStruct(_, seq) => translate_seq(seq),
+            Self::TupleVariant(_, _, variant, seq) => {
+                let mut obj = YamlObj::new();
+                obj.insert(YamlValue::String(variant.to_string()), translate_seq(seq));
+                YamlValue::Hash(obj)
+            }
+            Self::Map(map) => {
+                let map = map
+                    .iter()
+                    .map(|(k, v)| (k.as_yaml_blob(), v.as_yaml_blob()))
+                    .collect();
+
+                YamlValue::Hash(map)
+            }
+            Self::Struct(_name, fields) => translate_fields(fields),
+            Self::StructVariant(_, _, variant, fields) => {
+                let mut obj = YamlObj::new();
+                obj.insert(
+                    YamlValue::String(variant.to_string()),
+                    translate_fields(fields),
+                );
+                YamlValue::Hash(obj)
+            }
+        }
+    }
+}
diff --git a/src/content/mod.rs b/src/content/mod.rs
new file mode 100644
index 0000000..930ed69
--- /dev/null
+++ b/src/content/mod.rs
@@ -0,0 +1,373 @@
+//! this module is based on the content module in serde::private::ser
+
+mod error;
+mod formats;
+#[cfg(feature = "serialization")]
+mod serialization;
+pub(crate) mod utils;
+
+pub use error::*;
+#[cfg(feature = "serialization")]
+pub use serialization::*;
+
+/// Represents variable typed content.
+///
+/// This is used for the serialization system to represent values
+/// before the actual snapshots are written and is also exposed to
+/// dynamic redaction functions.
+///
+/// Some enum variants are intentionally not exposed to user code.
+/// It's generally recommended to construct content objects by
+/// using the [`From`](std::convert::From) trait and by using the
+/// accessor methods to assert on it.
+///
+/// While matching on the content is possible in theory it is
+/// recommended against.  The reason for this is that the content
+/// enum holds variants that can "wrap" values where it's not
+/// expected.  For instance if a field holds an `Option<String>`
+/// you cannot use pattern matching to extract the string as it
+/// will be contained in an internal `Some` variant that is not
+/// exposed.  On the other hand the `as_str` method will
+/// automatically resolve such internal wrappers.
+///
+/// If you do need to pattern match you should use the
+/// `resolve_inner` method to resolve such internal wrappers.
+#[derive(Debug, Clone, PartialEq, PartialOrd)]
+pub enum Content {
+    Bool(bool),
+
+    U8(u8),
+    U16(u16),
+    U32(u32),
+    U64(u64),
+    U128(u128),
+
+    I8(i8),
+    I16(i16),
+    I32(i32),
+    I64(i64),
+    I128(i128),
+
+    F32(f32),
+    F64(f64),
+
+    Char(char),
+    String(String),
+    Bytes(Vec<u8>),
+
+    #[doc(hidden)]
+    None,
+    #[doc(hidden)]
+    Some(Box<Content>),
+
+    #[doc(hidden)]
+    Unit,
+    #[doc(hidden)]
+    UnitStruct(&'static str),
+    #[doc(hidden)]
+    UnitVariant(&'static str, u32, &'static str),
+    #[doc(hidden)]
+    NewtypeStruct(&'static str, Box<Content>),
+    #[doc(hidden)]
+    NewtypeVariant(&'static str, u32, &'static str, Box<Content>),
+
+    Seq(Vec<Content>),
+    #[doc(hidden)]
+    Tuple(Vec<Content>),
+    #[doc(hidden)]
+    TupleStruct(&'static str, Vec<Content>),
+    #[doc(hidden)]
+    TupleVariant(&'static str, u32, &'static str, Vec<Content>),
+    Map(Vec<(Content, Content)>),
+    #[doc(hidden)]
+    Struct(&'static str, Vec<(&'static str, Content)>),
+    #[doc(hidden)]
+    StructVariant(
+        &'static str,
+        u32,
+        &'static str,
+        Vec<(&'static str, Content)>,
+    ),
+}
+
+macro_rules! impl_from {
+    ($ty:ty, $newty:ident) => {
+        impl From<$ty> for Content {
+            fn from(value: $ty) -> Content {
+                Content::$newty(value)
+            }
+        }
+    };
+}
+
+impl_from!(bool, Bool);
+impl_from!(u8, U8);
+impl_from!(u16, U16);
+impl_from!(u32, U32);
+impl_from!(u64, U64);
+impl_from!(u128, U128);
+impl_from!(i8, I8);
+impl_from!(i16, I16);
+impl_from!(i32, I32);
+impl_from!(i64, I64);
+impl_from!(i128, I128);
+impl_from!(f32, F32);
+impl_from!(f64, F64);
+impl_from!(char, Char);
+impl_from!(String, String);
+impl_from!(Vec<u8>, Bytes);
+
+impl From<()> for Content {
+    fn from(_value: ()) -> Content {
+        Content::Unit
+    }
+}
+
+impl<'a> From<&'a str> for Content {
+    fn from(value: &'a str) -> Content {
+        Content::String(value.to_string())
+    }
+}
+
+impl<'a> From<&'a [u8]> for Content {
+    fn from(value: &'a [u8]) -> Content {
+        Content::Bytes(value.to_vec())
+    }
+}
+
+impl Content {
+    /// This resolves the innermost content in a chain of
+    /// wrapped content.
+    ///
+    /// For instance if you encounter an `Option<Option<String>>`
+    /// field the content will be wrapped twice in an internal
+    /// option wrapper.  If you need to pattern match you will
+    /// need in some situations to first resolve the inner value
+    /// before such matching can take place as there is no exposed
+    /// way to match on these wrappers.
+    ///
+    /// This method does not need to be called for the `as_`
+    /// methods which resolve automatically.
+    pub fn resolve_inner(&self) -> &Content {
+        match *self {
+            Content::Some(ref v)
+            | Content::NewtypeStruct(_, ref v)
+            | Content::NewtypeVariant(_, _, _, ref v) => v.resolve_inner(),
+            ref other => other,
+        }
+    }
+
+    /// Mutable version of [`resolve_inner`](Self::resolve_inner).
+    pub fn resolve_inner_mut(&mut self) -> &mut Content {
+        match *self {
+            Content::Some(ref mut v)
+            | Content::NewtypeStruct(_, ref mut v)
+            | Content::NewtypeVariant(_, _, _, ref mut v) => v.resolve_inner_mut(),
+            ref mut other => other,
+        }
+    }
+
+    /// Returns the value as string
+    pub fn as_str(&self) -> Option<&str> {
+        match self.resolve_inner() {
+            Content::String(ref s) => Some(s.as_str()),
+            _ => None,
+        }
+    }
+
+    /// Returns the value as bytes
+    pub fn as_bytes(&self) -> Option<&[u8]> {
+        match self.resolve_inner() {
+            Content::Bytes(ref b) => Some(&*b),
+            _ => None,
+        }
+    }
+
+    /// Returns the value as slice of content values.
+    pub fn as_slice(&self) -> Option<&[Content]> {
+        match self.resolve_inner() {
+            Content::Seq(ref v) | Content::Tuple(ref v) | Content::TupleVariant(_, _, _, ref v) => {
+                Some(&v[..])
+            }
+            _ => None,
+        }
+    }
+
+    /// Returns true if the value is nil.
+    pub fn is_nil(&self) -> bool {
+        if let Content::None | Content::Unit = self.resolve_inner() {
+            true
+        } else {
+            false
+        }
+    }
+
+    /// Returns the value as bool
+    pub fn as_bool(&self) -> Option<bool> {
+        match *self.resolve_inner() {
+            Content::Bool(val) => Some(val),
+            _ => None,
+        }
+    }
+
+    /// Returns the value as u64
+    pub fn as_u64(&self) -> Option<u64> {
+        match *self.resolve_inner() {
+            Content::U8(v) => Some(u64::from(v)),
+            Content::U16(v) => Some(u64::from(v)),
+            Content::U32(v) => Some(u64::from(v)),
+            Content::U64(v) => Some(v),
+            Content::U128(v) => {
+                let rv = v as u64;
+                if rv as u128 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            Content::I8(v) if v >= 0 => Some(v as u64),
+            Content::I16(v) if v >= 0 => Some(v as u64),
+            Content::I32(v) if v >= 0 => Some(v as u64),
+            Content::I64(v) if v >= 0 => Some(v as u64),
+            Content::I128(v) => {
+                let rv = v as u64;
+                if rv as i128 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
+    /// Returns the value as u128
+    pub fn as_u128(&self) -> Option<u128> {
+        match *self.resolve_inner() {
+            Content::U128(v) => Some(v),
+            Content::I128(v) if v >= 0 => Some(v as u128),
+            _ => self.as_u64().map(u128::from),
+        }
+    }
+
+    /// Returns the value as i64
+    pub fn as_i64(&self) -> Option<i64> {
+        match *self.resolve_inner() {
+            Content::U8(v) => Some(i64::from(v)),
+            Content::U16(v) => Some(i64::from(v)),
+            Content::U32(v) => Some(i64::from(v)),
+            Content::U64(v) => {
+                let rv = v as i64;
+                if rv as u64 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            Content::U128(v) => {
+                let rv = v as i64;
+                if rv as u128 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            Content::I8(v) => Some(i64::from(v)),
+            Content::I16(v) => Some(i64::from(v)),
+            Content::I32(v) => Some(i64::from(v)),
+            Content::I64(v) => Some(v),
+            Content::I128(v) => {
+                let rv = v as i64;
+                if rv as i128 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
+    /// Returns the value as i128
+    pub fn as_i128(&self) -> Option<i128> {
+        match *self.resolve_inner() {
+            Content::U128(v) => {
+                let rv = v as i128;
+                if rv as u128 == v {
+                    Some(rv)
+                } else {
+                    None
+                }
+            }
+            Content::I128(v) => Some(v),
+            _ => self.as_i64().map(i128::from),
+        }
+    }
+
+    /// Returns the value as f64
+    pub fn as_f64(&self) -> Option<f64> {
+        match *self.resolve_inner() {
+            Content::F32(v) => Some(f64::from(v)),
+            Content::F64(v) => Some(v),
+            _ => None,
+        }
+    }
+
+    /// Recursively walks the content structure mutably.
+    ///
+    /// The callback is invoked for every content in the tree.
+    pub fn walk<F: FnMut(&mut Content) -> bool>(&mut self, visit: &mut F) {
+        if !visit(self) {
+            return;
+        }
+
+        match *self {
+            Content::Some(ref mut inner) => {
+                Self::walk(&mut *inner, visit);
+            }
+            Content::NewtypeStruct(_, ref mut inner) => {
+                Self::walk(&mut *inner, visit);
+            }
+            Content::NewtypeVariant(_, _, _, ref mut inner) => {
+                Self::walk(&mut *inner, visit);
+            }
+            Content::Seq(ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(inner, visit);
+                }
+            }
+            Content::Map(ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(&mut inner.0, visit);
+                    Self::walk(&mut inner.1, visit);
+                }
+            }
+            Content::Struct(_, ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(&mut inner.1, visit);
+                }
+            }
+            Content::StructVariant(_, _, _, ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(&mut inner.1, visit);
+                }
+            }
+            Content::Tuple(ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(inner, visit);
+                }
+            }
+            Content::TupleStruct(_, ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(inner, visit);
+                }
+            }
+            Content::TupleVariant(_, _, _, ref mut vec) => {
+                for inner in vec.iter_mut() {
+                    Self::walk(inner, visit);
+                }
+            }
+            _ => {}
+        }
+    }
+}
diff --git a/src/content.rs b/src/content/serialization.rs
similarity index 59%
rename from src/content.rs
rename to src/content/serialization.rs
index 9f67b12..47f31dd 100644
--- a/src/content.rs
+++ b/src/content/serialization.rs
@@ -1,87 +1,9 @@
-// this module is based on the content module in serde::private::ser
-use serde::ser::{self, Serialize, Serializer};
 use std::cmp::Ordering;
 use std::marker::PhantomData;
 
-/// Represents variable typed content.
-///
-/// This is used for the serialization system to represent values
-/// before the actual snapshots are written and is also exposed to
-/// dynamic redaction functions.
-///
-/// Some enum variants are intentionally not exposed to user code.
-/// It's generally recommended to construct content objects by
-/// using the [`From`](std::convert::From) trait and by using the
-/// accessor methods to assert on it.
-///
-/// While matching on the content is possible in theory it is
-/// recommended against.  The reason for this is that the content
-/// enum holds variants that can "wrap" values where it's not
-/// expected.  For instance if a field holds an `Option<String>`
-/// you cannot use pattern matching to extract the string as it
-/// will be contained in an internal `Some` variant that is not
-/// exposed.  On the other hand the `as_str` method will
-/// automatically resolve such internal wrappers.
-///
-/// If you do need to pattern match you should use the
-/// `resolve_inner` method to resolve such internal wrappers.
-#[derive(Debug, Clone, PartialEq, PartialOrd)]
-pub enum Content {
-    Bool(bool),
+use super::Content;
 
-    U8(u8),
-    U16(u16),
-    U32(u32),
-    U64(u64),
-    U128(u128),
-
-    I8(i8),
-    I16(i16),
-    I32(i32),
-    I64(i64),
-    I128(i128),
-
-    F32(f32),
-    F64(f64),
-
-    Char(char),
-    String(String),
-    Bytes(Vec<u8>),
-
-    #[doc(hidden)]
-    None,
-    #[doc(hidden)]
-    Some(Box<Content>),
-
-    #[doc(hidden)]
-    Unit,
-    #[doc(hidden)]
-    UnitStruct(&'static str),
-    #[doc(hidden)]
-    UnitVariant(&'static str, u32, &'static str),
-    #[doc(hidden)]
-    NewtypeStruct(&'static str, Box<Content>),
-    #[doc(hidden)]
-    NewtypeVariant(&'static str, u32, &'static str, Box<Content>),
-
-    Seq(Vec<Content>),
-    #[doc(hidden)]
-    Tuple(Vec<Content>),
-    #[doc(hidden)]
-    TupleStruct(&'static str, Vec<Content>),
-    #[doc(hidden)]
-    TupleVariant(&'static str, u32, &'static str, Vec<Content>),
-    Map(Vec<(Content, Content)>),
-    #[doc(hidden)]
-    Struct(&'static str, Vec<(&'static str, Content)>),
-    #[doc(hidden)]
-    StructVariant(
-        &'static str,
-        u32,
-        &'static str,
-        Vec<(&'static str, Content)>,
-    ),
-}
+use serde::{ser, Serialize, Serializer};
 
 #[derive(PartialEq, PartialOrd, Debug)]
 pub enum Key<'a> {
@@ -104,118 +26,7 @@ impl<'a> Ord for Key<'a> {
     }
 }
 
-macro_rules! impl_from {
-    ($ty:ty, $newty:ident) => {
-        impl From<$ty> for Content {
-            fn from(value: $ty) -> Content {
-                Content::$newty(value)
-            }
-        }
-    };
-}
-
-impl_from!(bool, Bool);
-impl_from!(u8, U8);
-impl_from!(u16, U16);
-impl_from!(u32, U32);
-impl_from!(u64, U64);
-impl_from!(u128, U128);
-impl_from!(i8, I8);
-impl_from!(i16, I16);
-impl_from!(i32, I32);
-impl_from!(i64, I64);
-impl_from!(i128, I128);
-impl_from!(f32, F32);
-impl_from!(f64, F64);
-impl_from!(char, Char);
-impl_from!(String, String);
-impl_from!(Vec<u8>, Bytes);
-
-impl From<()> for Content {
-    fn from(_value: ()) -> Content {
-        Content::Unit
-    }
-}
-
-impl<'a> From<&'a str> for Content {
-    fn from(value: &'a str) -> Content {
-        Content::String(value.to_string())
-    }
-}
-
-impl<'a> From<&'a [u8]> for Content {
-    fn from(value: &'a [u8]) -> Content {
-        Content::Bytes(value.to_vec())
-    }
-}
-
 impl Content {
-    /// This resolves the innermost content in a chain of
-    /// wrapped content.
-    ///
-    /// For instance if you encounter an `Option<Option<String>>`
-    /// field the content will be wrapped twice in an internal
-    /// option wrapper.  If you need to pattern match you will
-    /// need in some situations to first resolve the inner value
-    /// before such matching can take place as there is no exposed
-    /// way to match on these wrappers.
-    ///
-    /// This method does not need to be called for the `as_`
-    /// methods which resolve automatically.
-    pub fn resolve_inner(&self) -> &Content {
-        match *self {
-            Content::Some(ref v)
-            | Content::NewtypeStruct(_, ref v)
-            | Content::NewtypeVariant(_, _, _, ref v) => v.resolve_inner(),
-            ref other => other,
-        }
-    }
-
-    /// Mutable version of [`resolve_inner`](Self::resolve_inner).
-    pub fn resolve_inner_mut(&mut self) -> &mut Content {
-        match *self {
-            Content::Some(ref mut v)
-            | Content::NewtypeStruct(_, ref mut v)
-            | Content::NewtypeVariant(_, _, _, ref mut v) => v.resolve_inner_mut(),
-            ref mut other => other,
-        }
-    }
-
-    /// Returns the value as string
-    pub fn as_str(&self) -> Option<&str> {
-        match self.resolve_inner() {
-            Content::String(ref s) => Some(s.as_str()),
-            _ => None,
-        }
-    }
-
-    /// Returns the value as bytes
-    pub fn as_bytes(&self) -> Option<&[u8]> {
-        match self.resolve_inner() {
-            Content::Bytes(ref b) => Some(&*b),
-            _ => None,
-        }
-    }
-
-    /// Returns the value as slice of content values.
-    pub fn as_slice(&self) -> Option<&[Content]> {
-        match self.resolve_inner() {
-            Content::Seq(ref v) | Content::Tuple(ref v) | Content::TupleVariant(_, _, _, ref v) => {
-                Some(&v[..])
-            }
-            _ => None,
-        }
-    }
-
-    /// Returns true if the value is nil.
-    pub fn is_nil(&self) -> bool {
-        if let Content::None | Content::Unit = self.resolve_inner() {
-            true
-        } else {
-            false
-        }
-    }
-
     pub(crate) fn as_key(&self) -> Key<'_> {
         match *self.resolve_inner() {
             Content::Bool(val) => Key::Bool(val),
@@ -236,117 +47,6 @@ impl Content {
         }
     }
 
-    /// Returns the value as bool
-    pub fn as_bool(&self) -> Option<bool> {
-        match *self.resolve_inner() {
-            Content::Bool(val) => Some(val),
-            _ => None,
-        }
-    }
-
-    /// Returns the value as u64
-    pub fn as_u64(&self) -> Option<u64> {
-        match *self.resolve_inner() {
-            Content::U8(v) => Some(u64::from(v)),
-            Content::U16(v) => Some(u64::from(v)),
-            Content::U32(v) => Some(u64::from(v)),
-            Content::U64(v) => Some(v),
-            Content::U128(v) => {
-                let rv = v as u64;
-                if rv as u128 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            Content::I8(v) if v >= 0 => Some(v as u64),
-            Content::I16(v) if v >= 0 => Some(v as u64),
-            Content::I32(v) if v >= 0 => Some(v as u64),
-            Content::I64(v) if v >= 0 => Some(v as u64),
-            Content::I128(v) => {
-                let rv = v as u64;
-                if rv as i128 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
-    }
-
-    /// Returns the value as u128
-    pub fn as_u128(&self) -> Option<u128> {
-        match *self.resolve_inner() {
-            Content::U128(v) => Some(v),
-            Content::I128(v) if v >= 0 => Some(v as u128),
-            _ => self.as_u64().map(u128::from),
-        }
-    }
-
-    /// Returns the value as i64
-    pub fn as_i64(&self) -> Option<i64> {
-        match *self.resolve_inner() {
-            Content::U8(v) => Some(i64::from(v)),
-            Content::U16(v) => Some(i64::from(v)),
-            Content::U32(v) => Some(i64::from(v)),
-            Content::U64(v) => {
-                let rv = v as i64;
-                if rv as u64 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            Content::U128(v) => {
-                let rv = v as i64;
-                if rv as u128 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            Content::I8(v) => Some(i64::from(v)),
-            Content::I16(v) => Some(i64::from(v)),
-            Content::I32(v) => Some(i64::from(v)),
-            Content::I64(v) => Some(v),
-            Content::I128(v) => {
-                let rv = v as i64;
-                if rv as i128 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
-    }
-
-    /// Returns the value as i128
-    pub fn as_i128(&self) -> Option<i128> {
-        match *self.resolve_inner() {
-            Content::U128(v) => {
-                let rv = v as i128;
-                if rv as u128 == v {
-                    Some(rv)
-                } else {
-                    None
-                }
-            }
-            Content::I128(v) => Some(v),
-            _ => self.as_i64().map(i128::from),
-        }
-    }
-
-    /// Returns the value as f64
-    pub fn as_f64(&self) -> Option<f64> {
-        match *self.resolve_inner() {
-            Content::F32(v) => Some(f64::from(v)),
-            Content::F64(v) => Some(v),
-            _ => None,
-        }
-    }
-
     pub(crate) fn sort_maps(&mut self) {
         self.walk(&mut |content| {
             if let Content::Map(ref mut items) = content {
@@ -355,64 +55,6 @@ impl Content {
             true
         })
     }
-
-    /// Recursively walks the content structure mutably.
-    ///
-    /// The callback is invoked for every content in the tree.
-    pub fn walk<F: FnMut(&mut Content) -> bool>(&mut self, visit: &mut F) {
-        if !visit(self) {
-            return;
-        }
-
-        match *self {
-            Content::Some(ref mut inner) => {
-                Self::walk(&mut *inner, visit);
-            }
-            Content::NewtypeStruct(_, ref mut inner) => {
-                Self::walk(&mut *inner, visit);
-            }
-            Content::NewtypeVariant(_, _, _, ref mut inner) => {
-                Self::walk(&mut *inner, visit);
-            }
-            Content::Seq(ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(inner, visit);
-                }
-            }
-            Content::Map(ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(&mut inner.0, visit);
-                    Self::walk(&mut inner.1, visit);
-                }
-            }
-            Content::Struct(_, ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(&mut inner.1, visit);
-                }
-            }
-            Content::StructVariant(_, _, _, ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(&mut inner.1, visit);
-                }
-            }
-            Content::Tuple(ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(inner, visit);
-                }
-            }
-            Content::TupleStruct(_, ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(inner, visit);
-                }
-            }
-            Content::TupleVariant(_, _, _, ref mut vec) => {
-                for inner in vec.iter_mut() {
-                    Self::walk(inner, visit);
-                }
-            }
-            _ => {}
-        }
-    }
 }
 
 impl Serialize for Content {
diff --git a/src/content/utils.rs b/src/content/utils.rs
new file mode 100644
index 0000000..5be7d4a
--- /dev/null
+++ b/src/content/utils.rs
@@ -0,0 +1,65 @@
+use std::{collections::BTreeMap, convert::TryFrom};
+
+use super::{Content, Error, Result};
+
+pub fn into_unordered_struct_fields(
+    map: Vec<(Content, Content)>,
+) -> Result<BTreeMap<String, Content>> {
+    map.into_iter()
+        .map(|(k, v)| {
+            if let Content::String(s) = k {
+                Ok((s, v))
+            } else {
+                Err(Error::InvalidStructField(k))
+            }
+        })
+        .collect()
+}
+
+pub fn pop_str(map: &mut BTreeMap<String, Content>, key: &str) -> Result<String> {
+    match pop_nullable_str(map, key) {
+        Err(e) => Err(e),
+        Ok(None) => Err(Error::UnexpectedDataType),
+        Ok(Some(s)) => Ok(s),
+    }
+}
+
+pub fn pop_nullable_str(map: &mut BTreeMap<String, Content>, key: &str) -> Result<Option<String>> {
+    match map.remove(key) {
+        None => Ok(None),
+        Some(content) => {
+            if content.is_nil() {
+                Ok(None)
+            } else {
+                match content.as_str() {
+                    Some(s) => Ok(Some(s.to_owned())),
+                    None => Err(Error::UnexpectedDataType),
+                }
+            }
+        }
+    }
+}
+
+pub fn pop_u32(map: &mut BTreeMap<String, Content>, key: &str) -> Result<u32> {
+    match pop_nullable_u32(map, key) {
+        Err(e) => Err(e),
+        Ok(None) => Err(Error::UnexpectedDataType),
+        Ok(Some(num)) => Ok(num),
+    }
+}
+
+pub fn pop_nullable_u32(map: &mut BTreeMap<String, Content>, key: &str) -> Result<Option<u32>> {
+    match map.remove(key) {
+        None => Ok(None),
+        Some(content) => {
+            if content.is_nil() {
+                Ok(None)
+            } else {
+                match content.as_i64().and_then(|n| u32::try_from(n).ok()) {
+                    Some(n) => Ok(Some(n)),
+                    None => Err(Error::UnexpectedDataType),
+                }
+            }
+        }
+    }
+}
diff --git a/src/env.rs b/src/env.rs
index fc283d4..96a445f 100644
--- a/src/env.rs
+++ b/src/env.rs
@@ -5,7 +5,6 @@ use std::sync::{Arc, Mutex};
 use std::{env, fs};
 
 use once_cell::sync::Lazy;
-use serde::Deserialize;
 
 use crate::utils::is_ci;
 
@@ -106,10 +105,6 @@ pub fn get_cargo_workspace(manifest_dir: &str) -> Arc<PathBuf> {
         let path = if let Ok(workspace_root) = std::env::var("INSTA_WORKSPACE_ROOT") {
             Arc::new(PathBuf::from(workspace_root))
         } else {
-            #[derive(Deserialize)]
-            struct Manifest {
-                workspace_root: PathBuf,
-            }
             let output = std::process::Command::new(
                 env::var("CARGO")
                     .ok()
@@ -121,8 +116,9 @@ pub fn get_cargo_workspace(manifest_dir: &str) -> Arc<PathBuf> {
             .current_dir(manifest_dir)
             .output()
             .unwrap();
-            let manifest: Manifest = serde_json::from_slice(&output.stdout).unwrap();
-            Arc::new(manifest.workspace_root)
+            let manifest = dep_json::parse(std::str::from_utf8(&output.stdout).unwrap()).unwrap();
+            let workspace_root = PathBuf::from(manifest["workspace_root"].as_str().unwrap());
+            Arc::new(workspace_root)
         };
         workspaces.insert(manifest_dir.to_string(), path.clone());
         path
diff --git a/src/lib.rs b/src/lib.rs
index e97e2d6..12453b4 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -116,7 +116,8 @@
 //!
 //! Example:
 //!
-//! ```no_run
+#![cfg_attr(feature = "yaml", doc = " ```no_run")]
+#![cfg_attr(not(feature = "yaml"), doc = " ```ignore")]
 //! # use insta::*; use serde::Serialize;
 //! #[derive(Serialize)]
 //! pub struct User {
@@ -154,6 +155,7 @@ mod content;
 mod env;
 mod output;
 mod runtime;
+#[cfg(feature = "serialization")]
 mod serialization;
 mod settings;
 mod snapshot;
@@ -210,6 +212,8 @@ pub mod _macro_support {
     pub use crate::content::Content;
     pub use crate::env::get_cargo_workspace;
     pub use crate::runtime::{assert_snapshot, AutoName, ReferenceValue};
+
+    #[cfg(feature = "serde")]
     pub use crate::serialization::{serialize_value, SerializationFormat, SnapshotLocation};
 
     #[cfg(feature = "glob")]
diff --git a/src/macros.rs b/src/macros.rs
index e6a9e14..24500b1 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -25,8 +25,8 @@ macro_rules! _function_name {
 ///
 /// Example:
 ///
-/// ```no_run,ignore
-/// assert_csv_snapshot!(vec[1, 2, 3]);
+/// ```no_run
+/// insta::assert_csv_snapshot!(vec![1, 2, 3]);
 /// ```
 ///
 /// The third argument to the macro can be an object expression for redaction.
@@ -68,8 +68,8 @@ macro_rules! assert_csv_snapshot {
 ///
 /// Example:
 ///
-/// ```no_run,ignore
-/// assert_toml_snapshot!(vec[1, 2, 3]);
+/// ```no_run
+/// insta::assert_toml_snapshot!(vec![1, 2, 3]);
 /// ```
 ///
 /// The third argument to the macro can be an object expression for redaction.
@@ -142,6 +142,7 @@ macro_rules! assert_toml_snapshot {
 /// just use an empty string (`@""`).
 ///
 /// The snapshot name is optional but can be provided as first argument.
+#[cfg(feature = "yaml")]
 #[macro_export]
 macro_rules! assert_yaml_snapshot {
     ($value:expr, @$snapshot:literal) => {{
@@ -225,6 +226,7 @@ macro_rules! assert_ron_snapshot {
 /// about redactions refer to the [redactions feature in the guide](https://insta.rs/docs/redactions/).
 ///
 /// The snapshot name is optional but can be provided as first argument.
+#[cfg(feature = "json")]
 #[macro_export]
 macro_rules! assert_json_snapshot {
     ($value:expr, @$snapshot:literal) => {{
diff --git a/src/output.rs b/src/output.rs
index 2e151f9..31628c2 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -133,7 +133,8 @@ pub fn print_changeset(old: &str, new: &str, metadata: &MetaData, show_info: boo
         }
 
         if let Some(info) = metadata.private_info() {
-            let out = serde_yaml::to_string(&info).unwrap();
+            let out = info.as_yaml();
+            // TODO: does the yaml output always start with '---'?
             println!("{}", out.trim().strip_prefix("---").unwrap().trim_start());
             println!("{:─^1$}", "", width);
         }
diff --git a/src/runtime.rs b/src/runtime.rs
index d803ffc..91beca4 100644
--- a/src/runtime.rs
+++ b/src/runtime.rs
@@ -301,7 +301,7 @@ impl<'a> SnapshotAssertionContext<'a> {
                 } else {
                     Some(expr.to_string())
                 },
-                info: settings.info(),
+                info: settings.info().map(ToOwned::to_owned),
                 input_file: settings
                     .input_file()
                     .and_then(|x| self.localize_path(x))
@@ -508,11 +508,16 @@ pub fn assert_snapshot(
 /// Test snapshots in doctests.
 ///
 /// ```
-/// insta::assert_yaml_snapshot!("named", vec![1, 2, 3, 4, 5]);
+/// insta::assert_debug_snapshot!("named", vec![1, 2, 3, 4, 5]);
 /// ```
 ///
 /// ```should_panic
-/// insta::assert_yaml_snapshot!(vec![1, 2, 3, 4, 5]);
+/// insta::assert_debug_snapshot!(vec![1, 2, 3, 4, 5]);
+/// ```
+///
+/// ```
+/// let some_string = "Coucou je suis un joli bug";
+/// insta::assert_snapshot!(some_string, @"Coucou je suis un joli bug");
 /// ```
 ///
 /// ```
diff --git a/src/serialization.rs b/src/serialization.rs
index 4d98094..5281800 100644
--- a/src/serialization.rs
+++ b/src/serialization.rs
@@ -11,7 +11,9 @@ pub enum SerializationFormat {
     Ron,
     #[cfg(feature = "toml")]
     Toml,
+    #[cfg(feature = "yaml")]
     Yaml,
+    #[cfg(feature = "json")]
     Json,
 }
 
@@ -21,45 +23,47 @@ pub enum SnapshotLocation {
 }
 
 pub fn serialize_content(
-    mut content: Content,
+    mut _content: Content,
     format: SerializationFormat,
-    location: SnapshotLocation,
+    _location: SnapshotLocation,
 ) -> String {
-    content = Settings::with(|settings| {
+    _content = Settings::with(|settings| {
         if settings.sort_maps() {
-            content.sort_maps();
+            _content.sort_maps();
         }
         #[cfg(feature = "redactions")]
         {
             for (selector, redaction) in settings.iter_redactions() {
-                content = selector.redact(content, redaction);
+                _content = selector.redact(_content, redaction);
             }
         }
-        content
+        _content
     });
 
     match format {
+        #[cfg(feature = "yaml")]
         SerializationFormat::Yaml => {
-            let serialized = serde_yaml::to_string(&content).unwrap();
-            match location {
+            let serialized = serde_yaml::to_string(&_content).unwrap();
+            match _location {
                 SnapshotLocation::Inline => serialized,
                 SnapshotLocation::File => serialized[4..].to_string(),
             }
         }
-        SerializationFormat::Json => serde_json::to_string_pretty(&content).unwrap(),
+        #[cfg(feature = "json")]
+        SerializationFormat::Json => serde_json::to_string_pretty(&_content).unwrap(),
         #[cfg(feature = "csv")]
         SerializationFormat::Csv => {
             let mut buf = Vec::with_capacity(128);
             {
-                let mut writer = csv::Writer::from_writer(&mut buf);
+                let mut writer = dep_csv::Writer::from_writer(&mut buf);
                 // if the top-level content we're serializing is a vector we
                 // want to serialize it multiple times once for each item.
-                if let Some(content_slice) = content.as_slice() {
+                if let Some(content_slice) = _content.as_slice() {
                     for content in content_slice {
                         writer.serialize(content).unwrap();
                     }
                 } else {
-                    writer.serialize(&content).unwrap();
+                    writer.serialize(&_content).unwrap();
                 }
                 writer.flush().unwrap();
             }
@@ -71,22 +75,22 @@ pub fn serialize_content(
         #[cfg(feature = "ron")]
         SerializationFormat::Ron => {
             let mut buf = Vec::new();
-            let mut config = ron::ser::PrettyConfig::new();
+            let mut config = dep_ron::ser::PrettyConfig::new();
             config.new_line = "\n".to_string();
             config.indentor = "  ".to_string();
             config.struct_names = true;
-            let mut serializer = ron::ser::Serializer::with_options(
+            let mut serializer = dep_ron::ser::Serializer::with_options(
                 &mut buf,
                 Some(config),
-                ron::options::Options::default(),
+                dep_ron::options::Options::default(),
             )
             .unwrap();
-            content.serialize(&mut serializer).unwrap();
+            _content.serialize(&mut serializer).unwrap();
             String::from_utf8(buf).unwrap()
         }
         #[cfg(feature = "toml")]
         SerializationFormat::Toml => {
-            let mut rv = toml::to_string_pretty(&content).unwrap();
+            let mut rv = dep_toml::to_string_pretty(&_content).unwrap();
             if rv.ends_with('\n') {
                 rv.truncate(rv.len() - 1);
             }
diff --git a/src/settings.rs b/src/settings.rs
index 69a3286..3cc16ac 100644
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -7,16 +7,16 @@ use std::sync::Arc;
 use std::task::{Context, Poll};
 
 use once_cell::sync::Lazy;
-use serde::Serialize;
-
-#[cfg(feature = "redactions")]
-use crate::{
-    content::Content,
-    redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector},
-};
+#[cfg(feature = "serialization")]
+use serde::{de::value::Error as ValueError, Serialize};
 
+use crate::content::Content;
+#[cfg(feature = "serialization")]
+use crate::content::ContentSerializer;
 #[cfg(feature = "filters")]
 use crate::filters::Filters;
+#[cfg(feature = "redactions")]
+use crate::redaction::{dynamic_redaction, sorted_redaction, ContentPath, Redaction, Selector};
 
 static DEFAULT_SETTINGS: Lazy<Arc<ActualSettings>> = Lazy::new(|| {
     Arc::new(ActualSettings {
@@ -63,7 +63,7 @@ pub struct ActualSettings {
     pub snapshot_suffix: String,
     pub input_file: Option<PathBuf>,
     pub description: Option<String>,
-    pub info: Option<serde_yaml::Value>,
+    pub info: Option<Content>,
     pub omit_expression: bool,
     pub prepend_module_to_snapshot: bool,
     #[cfg(feature = "redactions")]
@@ -95,8 +95,15 @@ impl ActualSettings {
         self.description = Some(value.into());
     }
 
-    pub fn info<S: Serialize>(&mut self, value: &S) {
-        self.info = Some(serde_yaml::to_value(value).unwrap());
+    #[cfg(feature = "serialization")]
+    pub fn info<S: Serialize>(&mut self, s: &S) {
+        let serializer = ContentSerializer::<ValueError>::new();
+        let content = Serialize::serialize(s, serializer).unwrap();
+        self.info = Some(content);
+    }
+
+    pub fn raw_info(&mut self, content: &Content) {
+        self.info = Some(content.to_owned());
     }
 
     pub fn omit_expression(&mut self, value: bool) {
@@ -310,8 +317,13 @@ impl Settings {
     ///
     /// As an example the input paramters to the function that creates the snapshot
     /// can be persisted here.
-    pub fn set_info<S: Serialize>(&mut self, value: &S) {
-        self._private_inner_mut().info(value);
+    #[cfg(feature = "serialization")]
+    pub fn set_info<S: Serialize>(&mut self, s: &S) {
+        self._private_inner_mut().info(s);
+    }
+
+    pub fn set_raw_info(&mut self, content: &Content) {
+        self._private_inner_mut().raw_info(content);
     }
 
     /// Removes the info.
@@ -320,8 +332,8 @@ impl Settings {
     }
 
     /// Returns the current info
-    pub(crate) fn info(&self) -> Option<serde_yaml::Value> {
-        self.inner.info.clone()
+    pub(crate) fn info(&self) -> Option<&Content> {
+        self.inner.info.as_ref()
     }
 
     /// Returns the current info
diff --git a/src/snapshot.rs b/src/snapshot.rs
index 8108d3a..95bd306 100644
--- a/src/snapshot.rs
+++ b/src/snapshot.rs
@@ -6,8 +6,9 @@ use std::io::{BufRead, BufReader, Write};
 use std::path::{Path, PathBuf};
 use std::time::{SystemTime, UNIX_EPOCH};
 
+use crate::content::{self, Content};
+
 use once_cell::sync::Lazy;
-use serde::{Deserialize, Serialize};
 
 static RUN_ID: Lazy<String> = Lazy::new(|| {
     dbg!(if let Ok(run_id) = env::var("NEXTEST_RUN_ID") {
@@ -18,7 +19,7 @@ static RUN_ID: Lazy<String> = Lazy::new(|| {
     })
 });
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug)]
 pub struct PendingInlineSnapshot {
     pub run_id: String,
     pub line: u32,
@@ -37,9 +38,15 @@ impl PendingInlineSnapshot {
     }
 
     pub fn load_batch<P: AsRef<Path>>(p: P) -> Result<Vec<PendingInlineSnapshot>, Box<dyn Error>> {
-        let f = BufReader::new(fs::File::open(p)?);
-        let iter = serde_json::Deserializer::from_reader(f).into_iter::<PendingInlineSnapshot>();
-        let mut rv = iter.collect::<Result<Vec<PendingInlineSnapshot>, _>>()?;
+        let contents = fs::read_to_string(p)?;
+
+        let mut rv: Vec<Self> = contents
+            .lines()
+            .map(|line| {
+                let value = Content::from_json(line)?;
+                Self::from_content(value)
+            })
+            .collect::<Result<_, Box<dyn Error>>>()?;
 
         // remove all but the last run
         if let Some(last_run_id) = rv.last().map(|x| x.run_id.clone()) {
@@ -62,33 +69,76 @@ impl PendingInlineSnapshot {
 
     pub fn save<P: AsRef<Path>>(&self, p: P) -> Result<(), Box<dyn Error>> {
         let mut f = fs::OpenOptions::new().create(true).append(true).open(p)?;
-        let mut s = serde_json::to_string(self)?;
+        let mut s = self.as_content().as_json()?;
         s.push('\n');
         f.write_all(s.as_bytes())?;
         Ok(())
     }
+
+    fn from_content(content: Content) -> Result<PendingInlineSnapshot, Box<dyn Error>> {
+        if let Content::Map(map) = content {
+            let mut map = content::utils::into_unordered_struct_fields(map)?;
+
+            let run_id = content::utils::pop_str(&mut map, "run_id")?;
+            let line = content::utils::pop_u32(&mut map, "line")?;
+            let new = match map.remove("new") {
+                None | Some(Content::None) => None,
+                Some(non_null) => Some(Snapshot::from_content(non_null)?),
+            };
+            let old = match map.remove("old") {
+                None | Some(Content::None) => None,
+                Some(non_null) => Some(Snapshot::from_content(non_null)?),
+            };
+
+            Ok(PendingInlineSnapshot {
+                run_id,
+                line,
+                new,
+                old,
+            })
+        } else {
+            Err(content::Error::UnexpectedDataType.into())
+        }
+    }
+
+    fn as_content(&self) -> Content {
+        let fields = vec![
+            ("run_id", Content::from(self.run_id.as_str())),
+            ("line", Content::from(self.line)),
+            (
+                "new",
+                match &self.new {
+                    Some(snap) => snap.as_content(),
+                    None => Content::None,
+                },
+            ),
+            (
+                "old",
+                match &self.old {
+                    Some(snap) => snap.as_content(),
+                    None => Content::None,
+                },
+            ),
+        ];
+
+        Content::Struct("PendingInlineSnapshot", fields)
+    }
 }
 
 /// Snapshot metadata information.
-#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+#[derive(Debug, Default, Clone)]
 pub struct MetaData {
     /// The source file (relative to workspace root).
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub(crate) source: Option<String>,
     /// The source line if available.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub(crate) assertion_line: Option<u32>,
     /// Optional human readable (non formatted) snapshot description.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub(crate) description: Option<String>,
     /// Optionally the expression that created the snapshot.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub(crate) expression: Option<String>,
     /// An optional arbitrary structured info object.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub(crate) info: Option<serde_yaml::Value>,
+    pub(crate) info: Option<Content>,
     /// Reference to the input file.
-    #[serde(skip_serializing_if = "Option::is_none")]
     pub(crate) input_file: Option<String>,
 }
 
@@ -115,7 +165,7 @@ impl MetaData {
 
     /// Returns the embedded info.
     #[doc(hidden)]
-    pub fn private_info(&self) -> Option<&serde_yaml::Value> {
+    pub fn private_info(&self) -> Option<&Content> {
         self.info.as_ref()
     }
 
@@ -134,13 +184,60 @@ impl MetaData {
     pub fn input_file(&self) -> Option<&str> {
         self.input_file.as_deref()
     }
+
+    fn from_content(content: Content) -> Result<MetaData, Box<dyn Error>> {
+        if let Content::Map(map) = content {
+            let mut map = content::utils::into_unordered_struct_fields(map)?;
+
+            let source = content::utils::pop_nullable_str(&mut map, "source")?;
+            let assertion_line = content::utils::pop_nullable_u32(&mut map, "assertion_line")?;
+            let description = content::utils::pop_nullable_str(&mut map, "description")?;
+            let expression = content::utils::pop_nullable_str(&mut map, "expression")?;
+            let info = map.remove("info");
+            let input_file = content::utils::pop_nullable_str(&mut map, "input_file")?;
+
+            Ok(MetaData {
+                source,
+                assertion_line,
+                description,
+                expression,
+                info,
+                input_file,
+            })
+        } else {
+            Err(content::Error::UnexpectedDataType.into())
+        }
+    }
+
+    fn as_content(&self) -> Content {
+        let mut fields = Vec::new();
+        if let Some(source) = self.source.as_deref() {
+            fields.push(("source", Content::from(source)));
+        }
+        if let Some(expression) = self.expression.as_deref() {
+            fields.push(("expression", Content::from(expression)));
+        }
+        if let Some(line) = self.assertion_line {
+            fields.push(("assertion_line", Content::from(line)));
+        }
+        if let Some(description) = self.description.as_deref() {
+            fields.push(("description", Content::from(description)));
+        }
+        if let Some(info) = &self.info {
+            fields.push(("info", info.to_owned()));
+        }
+        if let Some(input_file) = self.input_file.as_deref() {
+            fields.push(("input_file", Content::from(input_file)));
+        }
+
+        Content::Struct("MetaData", fields)
+    }
 }
 
 /// A helper to work with stored snapshots.
-#[derive(Debug, Serialize, Deserialize, Clone)]
+#[derive(Debug, Clone)]
 pub struct Snapshot {
     module_name: String,
-    #[serde(skip_serializing_if = "Option::is_none")]
     snapshot_name: Option<String>,
     metadata: MetaData,
     snapshot: SnapshotContents,
@@ -166,7 +263,8 @@ impl Snapshot {
                     break;
                 }
             }
-            serde_yaml::from_str(&buf)?
+            let content = Content::from_yaml(&buf)?;
+            MetaData::from_content(content)?
         // legacy format
         } else {
             let mut rv = MetaData::default();
@@ -248,6 +346,39 @@ impl Snapshot {
         }
     }
 
+    fn from_content(content: Content) -> Result<Snapshot, Box<dyn Error>> {
+        if let Content::Map(map) = content {
+            let mut map = content::utils::into_unordered_struct_fields(map)?;
+
+            let module_name = content::utils::pop_str(&mut map, "module_name")?;
+            let snapshot_name = content::utils::pop_nullable_str(&mut map, "snapshot_name")?;
+            let metadata = MetaData::from_content(
+                map.remove("metadata").ok_or(content::Error::MissingField)?,
+            )?;
+            let snapshot = SnapshotContents(content::utils::pop_str(&mut map, "snapshot")?);
+
+            Ok(Snapshot {
+                module_name,
+                snapshot_name,
+                metadata,
+                snapshot,
+            })
+        } else {
+            Err(content::Error::UnexpectedDataType.into())
+        }
+    }
+
+    fn as_content(&self) -> Content {
+        let mut fields = vec![("module_name", Content::from(self.module_name.as_str()))];
+        if let Some(name) = self.snapshot_name.as_deref() {
+            fields.push(("snapshot_name", Content::from(name)));
+        }
+        fields.push(("metadata", self.metadata.as_content()));
+        fields.push(("snapshot", Content::from(self.snapshot.0.as_str())));
+
+        Content::Struct("Content", fields)
+    }
+
     /// Returns the module name.
     pub fn module_name(&self) -> &str {
         &self.module_name
@@ -283,7 +414,8 @@ impl Snapshot {
             fs::create_dir_all(&folder)?;
         }
         let mut f = fs::File::create(&path)?;
-        serde_yaml::to_writer(&mut f, md)?;
+        let blob = md.as_content().as_yaml();
+        f.write_all(blob.as_bytes())?;
         f.write_all(b"---\n")?;
         f.write_all(self.contents_str().as_bytes())?;
         f.write_all(b"\n")?;
@@ -312,7 +444,7 @@ impl Snapshot {
 
 /// The contents of a Snapshot
 // Could be Cow, but I think limited savings
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone)]
 pub struct SnapshotContents(String);
 
 impl SnapshotContents {


Test patch:
diff --git a/cargo-insta/integration-tests/Cargo.toml b/cargo-insta/integration-tests/Cargo.toml
index b401e32..70fd186 100644
--- a/cargo-insta/integration-tests/Cargo.toml
+++ b/cargo-insta/integration-tests/Cargo.toml
@@ -8,6 +8,6 @@ edition = "2018"
 
 [dependencies]
 dircpy = "0.3.4"
-insta = { version = "1.1.0", path = "../..", features = ["redactions", "glob"] }
+insta = { version = "1.1.0", path = "../..", features = ["json", "yaml", "redactions", "glob"] }
 walkdir = "2.3.1"
 serde = { version = "1.0.117", features = ["derive"] }
diff --git a/src/snapshots/doctest_runtime_rs__named.snap b/src/snapshots/doctest_runtime_rs__named.snap
index de3eb3f..d3686a7 100644
--- a/src/snapshots/doctest_runtime_rs__named.snap
+++ b/src/snapshots/doctest_runtime_rs__named.snap
@@ -2,9 +2,10 @@
 source: src/runtime.rs
 expression: "vec![1, 2, 3, 4, 5]"
 ---
-- 1
-- 2
-- 3
-- 4
-- 5
-
+[
+    1,
+    2,
+    3,
+    4,
+    5,
+]
diff --git a/tests/snapshots/snapshot_no_module_prepending.snap b/tests/snapshots/snapshot_no_module_prepending.snap
index da2242a..0dc875c 100644
--- a/tests/snapshots/snapshot_no_module_prepending.snap
+++ b/tests/snapshots/snapshot_no_module_prepending.snap
@@ -2,7 +2,8 @@
 source: tests/test_settings.rs
 expression: "vec![1, 2, 3]"
 ---
-- 1
-- 2
-- 3
-
+[
+    1,
+    2,
+    3,
+]
diff --git a/tests/snapshots/test_redaction__user.snap b/tests/snapshots/test_redaction__user.snap
index 8ab9b42..276962e 100644
--- a/tests/snapshots/test_redaction__user.snap
+++ b/tests/snapshots/test_redaction__user.snap
@@ -1,6 +1,6 @@
 ---
 source: tests/test_redaction.rs
-expression: "&User {\n        id: 11,\n        username: \"john_doe\".to_string(),\n        email: Email(\"john@example.com\".to_string()),\n        extra: \"\".to_string(),\n    }"
+expression: "&User {\n        id: 23,\n        username: \"john_doe\".to_string(),\n        email: Email(\"john@example.com\".to_string()),\n        extra: \"\".to_string(),\n    }"
 ---
 id: "[id]"
 username: john_doe
diff --git a/tests/snapshots/test_settings__snapshot_with_description.snap b/tests/snapshots/test_settings__snapshot_with_description.snap
index 7f9b974..e94ccdb 100644
--- a/tests/snapshots/test_settings__snapshot_with_description.snap
+++ b/tests/snapshots/test_settings__snapshot_with_description.snap
@@ -1,9 +1,10 @@
 ---
 source: tests/test_settings.rs
-description: The snapshot are three integers
+description: The snapshot is three integers
 expression: "vec![1, 2, 3]"
 ---
-- 1
-- 2
-- 3
-
+[
+    1,
+    2,
+    3,
+]
diff --git a/tests/snapshots/test_settings__snapshot_with_description_and_info.snap b/tests/snapshots/test_settings__snapshot_with_description_and_info.snap
index 0242ce4..1e1acd0 100644
--- a/tests/snapshots/test_settings__snapshot_with_description_and_info.snap
+++ b/tests/snapshots/test_settings__snapshot_with_description_and_info.snap
@@ -1,6 +1,6 @@
 ---
 source: tests/test_settings.rs
-description: The snapshot are four integers
+description: The snapshot is four integers
 expression: "vec![1, 2, 3, 4]"
 info:
   env:
@@ -9,8 +9,9 @@ info:
     - my-tool
     - run
 ---
-- 1
-- 2
-- 3
-- 4
-
+[
+    1,
+    2,
+    3,
+    4,
+]
diff --git a/tests/snapshots/test_settings__snapshot_with_description_and_raw_info.snap b/tests/snapshots/test_settings__snapshot_with_description_and_raw_info.snap
new file mode 100644
index 0000000..14349e1
--- /dev/null
+++ b/tests/snapshots/test_settings__snapshot_with_description_and_raw_info.snap
@@ -0,0 +1,18 @@
+---
+source: tests/test_settings.rs
+description: The snapshot is four integers
+expression: "vec![1, 2, 3, 4]"
+info:
+  env:
+    - ENVIRONMENT
+    - production
+  cmdline:
+    - my-tool
+    - run
+---
+[
+    1,
+    2,
+    3,
+    4,
+]
diff --git a/tests/snapshots2/test_settings__snapshot_path.snap b/tests/snapshots2/test_settings__snapshot_path.snap
index da2242a..0dc875c 100644
--- a/tests/snapshots2/test_settings__snapshot_path.snap
+++ b/tests/snapshots2/test_settings__snapshot_path.snap
@@ -2,7 +2,8 @@
 source: tests/test_settings.rs
 expression: "vec![1, 2, 3]"
 ---
-- 1
-- 2
-- 3
-
+[
+    1,
+    2,
+    3,
+]
diff --git a/tests/test_basic.rs b/tests/test_basic.rs
index 7422c38..2448cd4 100644
--- a/tests/test_basic.rs
+++ b/tests/test_basic.rs
@@ -1,6 +1,8 @@
-use insta::{
-    assert_debug_snapshot, assert_display_snapshot, assert_json_snapshot, assert_yaml_snapshot,
-};
+#[cfg(feature = "json")]
+use insta::assert_json_snapshot;
+#[cfg(feature = "yaml")]
+use insta::assert_yaml_snapshot;
+use insta::{assert_debug_snapshot, assert_display_snapshot};
 use std::fmt;
 
 #[test]
@@ -25,11 +27,13 @@ fn test_unnamed_nested_closure() {
     })();
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_yaml_vector() {
     assert_yaml_snapshot!("yaml_vector", vec![1, 2, 3]);
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_unnamed_yaml_vector() {
     assert_yaml_snapshot!(vec![1, 2, 3]);
@@ -37,11 +41,13 @@ fn test_unnamed_yaml_vector() {
     assert_yaml_snapshot!(vec![1, 2, 3, 4, 5]);
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_json_vector() {
     assert_json_snapshot!("json_vector", vec![1, 2, 3]);
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_unnamed_json_vector() {
     assert_json_snapshot!(vec![1, 2, 3]);
@@ -77,8 +83,9 @@ fn test_unnamed_display() {
     assert_display_snapshot!("whatever");
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_u128_json() {
-    let x: u128 = 42;
-    assert_json_snapshot!(&x, @"42");
+    let x: u128 = u128::from(u64::MAX) * 2;
+    assert_json_snapshot!(&x, @"36893488147419103230");
 }
diff --git a/tests/test_inline.rs b/tests/test_inline.rs
index f4cf9c2..e6ebdf0 100644
--- a/tests/test_inline.rs
+++ b/tests/test_inline.rs
@@ -1,11 +1,15 @@
 #[cfg(feature = "csv")]
 use insta::assert_csv_snapshot;
+#[cfg(feature = "json")]
+use insta::assert_json_snapshot;
 #[cfg(feature = "ron")]
 use insta::assert_ron_snapshot;
 #[cfg(feature = "toml")]
 use insta::assert_toml_snapshot;
-use insta::{assert_debug_snapshot, assert_json_snapshot, assert_snapshot, assert_yaml_snapshot};
-use serde::Serialize;
+#[cfg(feature = "yaml")]
+use insta::assert_yaml_snapshot;
+
+use insta::{assert_debug_snapshot, assert_snapshot};
 use std::thread;
 
 #[test]
@@ -58,10 +62,10 @@ fn test_newline() {
 #[cfg(feature = "csv")]
 #[test]
 fn test_csv_inline() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct Email(String);
 
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
@@ -81,10 +85,10 @@ fn test_csv_inline() {
 #[cfg(feature = "csv")]
 #[test]
 fn test_csv_inline_multiple_values() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct Email(String);
 
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
@@ -112,10 +116,10 @@ fn test_csv_inline_multiple_values() {
 #[cfg(feature = "ron")]
 #[test]
 fn test_ron_inline() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct Email(String);
 
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
@@ -138,10 +142,10 @@ fn test_ron_inline() {
 #[cfg(feature = "toml")]
 #[test]
 fn test_toml_inline() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct Email(String);
 
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
@@ -159,6 +163,7 @@ fn test_toml_inline() {
     "###);
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_json_inline() {
     assert_json_snapshot!(vec!["foo", "bar"], @r###"
@@ -169,9 +174,10 @@ fn test_json_inline() {
     "###);
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_yaml_inline() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
@@ -190,10 +196,10 @@ fn test_yaml_inline() {
     "###);
 }
 
-#[cfg(feature = "redactions")]
+#[cfg(all(feature = "redactions", feature = "yaml"))]
 #[test]
 fn test_yaml_inline_redacted() {
-    #[derive(Serialize)]
+    #[derive(serde::Serialize)]
     pub struct User {
         id: u32,
         username: String,
diff --git a/tests/test_redaction.rs b/tests/test_redaction.rs
index e0c2f56..4e4ec92 100644
--- a/tests/test_redaction.rs
+++ b/tests/test_redaction.rs
@@ -1,15 +1,19 @@
 #![cfg(feature = "redactions")]
 
-use std::collections::{BTreeMap, HashMap, HashSet};
-
 use insta::_macro_support::Selector;
-use insta::{
-    assert_debug_snapshot, assert_json_snapshot, assert_yaml_snapshot, sorted_redaction,
-    with_settings, Settings,
-};
-use serde::Serialize;
+#[cfg(feature = "csv")]
+use insta::assert_csv_snapshot;
+#[cfg(feature = "json")]
+use insta::assert_json_snapshot;
+#[cfg(feature = "ron")]
+use insta::assert_ron_snapshot;
+#[cfg(feature = "toml")]
+use insta::assert_toml_snapshot;
+#[cfg(feature = "yaml")]
+use insta::assert_yaml_snapshot;
 
-use similar_asserts::assert_eq;
+use insta::assert_debug_snapshot;
+use serde::Serialize;
 
 #[test]
 fn test_selector_parser() {
@@ -39,6 +43,7 @@ pub struct User {
     extra: String,
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_with_random_value() {
     assert_yaml_snapshot!("user", &User {
@@ -51,6 +56,7 @@ fn test_with_random_value() {
     });
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_with_random_value_inline_callback() {
     assert_yaml_snapshot!("user", &User {
@@ -60,13 +66,14 @@ fn test_with_random_value_inline_callback() {
         extra: "".to_string(),
     }, {
         ".id" => insta::dynamic_redaction(|value, path| {
-            assert_eq!(path.to_string(), ".id");
-            assert_eq!(value.as_u64().unwrap(), 23);
+            similar_asserts::assert_eq!(path.to_string(), ".id");
+            similar_asserts::assert_eq!(value.as_u64().unwrap(), 23);
             "[id]"
         }),
     });
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_with_random_value_and_trailing_comma() {
     assert_yaml_snapshot!("user", &User {
@@ -82,7 +89,7 @@ fn test_with_random_value_and_trailing_comma() {
 #[cfg(feature = "csv")]
 #[test]
 fn test_with_random_value_csv() {
-    insta::assert_csv_snapshot!("user_csv", &User {
+    assert_csv_snapshot!("user_csv", &User {
         id: 44,
         username: "julius_csv".to_string(),
         email: Email("julius@example.com".to_string()),
@@ -95,7 +102,7 @@ fn test_with_random_value_csv() {
 #[cfg(feature = "ron")]
 #[test]
 fn test_with_random_value_ron() {
-    insta::assert_ron_snapshot!("user_ron", &User {
+    assert_ron_snapshot!("user_ron", &User {
         id: 53,
         username: "john_ron".to_string(),
         email: Email("john@example.com".to_string()),
@@ -108,7 +115,7 @@ fn test_with_random_value_ron() {
 #[cfg(feature = "toml")]
 #[test]
 fn test_with_random_value_toml() {
-    insta::assert_toml_snapshot!("user_toml", &User {
+    assert_toml_snapshot!("user_toml", &User {
         id: 53,
         username: "john_ron".to_string(),
         email: Email("john@example.com".to_string()),
@@ -118,6 +125,7 @@ fn test_with_random_value_toml() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_with_random_value_json() {
     assert_json_snapshot!("user_json", &User {
@@ -131,9 +139,10 @@ fn test_with_random_value_json() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_with_random_value_json_settings() {
-    let mut settings = Settings::new();
+    let mut settings = insta::Settings::new();
     settings.add_redaction(".id", "[id]");
     settings.add_redaction(".extra", "[extra]");
     settings.bind(|| {
@@ -149,12 +158,13 @@ fn test_with_random_value_json_settings() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_with_callbacks() {
-    let mut settings = Settings::new();
+    let mut settings = insta::Settings::new();
     settings.add_dynamic_redaction(".id", |value, path| {
-        assert_eq!(path.to_string(), ".id");
-        assert_eq!(value.as_u64().unwrap(), 1234);
+        similar_asserts::assert_eq!(path.to_string(), ".id");
+        similar_asserts::assert_eq!(value.as_u64().unwrap(), 1234);
         "[id]"
     });
     settings.bind(|| {
@@ -170,9 +180,10 @@ fn test_with_callbacks() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_with_random_value_json_settings2() {
-    with_settings!({redactions => vec![
+    insta::with_settings!({redactions => vec![
         (".id", "[id]".into()),
         (".extra", "[extra]".into()),
     ]}, {
@@ -187,6 +198,7 @@ fn test_with_random_value_json_settings2() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_redact_newtype_struct() {
     #[derive(Serialize)]
@@ -211,6 +223,7 @@ fn test_redact_newtype_struct() {
     "###);
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_redact_newtype_enum() {
     #[derive(Serialize)]
@@ -250,6 +263,7 @@ fn test_redact_newtype_enum() {
     "###);
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_redact_recursive() {
     #[derive(Serialize)]
@@ -276,6 +290,7 @@ fn test_redact_recursive() {
     "###);
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_struct_array_redaction() {
     #[derive(Serialize)]
@@ -304,13 +319,14 @@ fn test_struct_array_redaction() {
         ],
     };
 
-    insta::assert_yaml_snapshot!(vec![checkout], {
+    assert_yaml_snapshot!(vec![checkout], {
         "[]._id" => "[checkout_id]",
         "[].products[]._id" => "[product_id]",
         "[].products[].product_name" => "[product_name]",
     });
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_map_key_redaction() {
     #[derive(Serialize, Hash, PartialEq, PartialOrd, Eq, Ord)]
@@ -321,11 +337,11 @@ fn test_map_key_redaction() {
 
     #[derive(Serialize)]
     struct Foo {
-        hm: HashMap<Key, u32>,
-        btm: BTreeMap<(u32, u32), u32>,
+        hm: std::collections::HashMap<Key, u32>,
+        btm: std::collections::BTreeMap<(u32, u32), u32>,
     }
 
-    let mut hm = HashMap::new();
+    let mut hm = std::collections::HashMap::new();
     hm.insert(
         Key {
             bucket: 1,
@@ -333,26 +349,27 @@ fn test_map_key_redaction() {
         },
         42,
     );
-    let mut btm = BTreeMap::new();
+    let mut btm = std::collections::BTreeMap::new();
     btm.insert((0, 0), 23);
     let foo_value = Foo { hm, btm };
 
-    insta::assert_yaml_snapshot!(foo_value, {
+    assert_yaml_snapshot!(foo_value, {
         ".hm.$key.bucket" => "[bucket]",
         ".btm.$key" => "[key]",
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_ordering() {
     #[derive(Debug, Serialize)]
     pub struct User {
         id: u64,
         username: String,
-        flags: HashSet<String>,
+        flags: std::collections::HashSet<String>,
     }
 
-    let mut settings = Settings::new();
+    let mut settings = insta::Settings::new();
     settings.add_redaction(".id", "[id]");
     settings.sort_selector(".flags");
     settings.bind(|| {
@@ -369,10 +386,11 @@ fn test_ordering() {
     });
 }
 
+#[cfg(feature = "json")]
 #[test]
 fn test_ordering_newtype_set() {
     #[derive(Debug, Serialize)]
-    pub struct MySet(HashSet<String>);
+    pub struct MySet(std::collections::HashSet<String>);
 
     #[derive(Debug, Serialize)]
     pub struct User {
@@ -391,8 +409,8 @@ fn test_ordering_newtype_set() {
                 .collect()),
         },
         {
-            "." => sorted_redaction(),
-            ".flags" => sorted_redaction()
+            "." => insta::sorted_redaction(),
+            ".flags" => insta::sorted_redaction()
         }
     );
 }
diff --git a/tests/test_settings.rs b/tests/test_settings.rs
index c82d22d..2431c23 100644
--- a/tests/test_settings.rs
+++ b/tests/test_settings.rs
@@ -1,16 +1,19 @@
-use insta::{assert_yaml_snapshot, with_settings, Settings};
+#[cfg(feature = "yaml")]
+use insta::assert_yaml_snapshot;
 use similar_asserts::assert_eq;
-use std::collections::HashMap;
 
+use insta::{assert_debug_snapshot, with_settings, Settings};
+
+#[cfg(feature = "yaml")]
 #[test]
 fn test_simple() {
-    let mut map = HashMap::new();
+    let mut map = std::collections::HashMap::new();
     map.insert("a", "first value");
     map.insert("b", "second value");
     map.insert("c", "third value");
     map.insert("d", "fourth value");
 
-    let mut settings = Settings::new();
+    let mut settings = insta::Settings::new();
     settings.set_sort_maps(true);
     settings.bind(|| {
         assert_yaml_snapshot!(&map, @r###"
@@ -23,10 +26,11 @@ fn test_simple() {
     });
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 #[allow(deprecated)]
 fn test_bound_to_thread() {
-    let mut map = HashMap::new();
+    let mut map = std::collections::HashMap::new();
     map.insert("a", "first value");
     map.insert("b", "second value");
     map.insert("c", "third value");
@@ -48,9 +52,10 @@ fn test_bound_to_thread() {
     settings.bind_to_thread();
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_bound_to_scope() {
-    let mut map = HashMap::new();
+    let mut map = std::collections::HashMap::new();
     map.insert("a", "first value");
     map.insert("b", "second value");
     map.insert("c", "third value");
@@ -72,9 +77,10 @@ fn test_bound_to_scope() {
     assert!(!Settings::clone_current().sort_maps());
 }
 
+#[cfg(feature = "yaml")]
 #[test]
 fn test_settings_macro() {
-    let mut map = HashMap::new();
+    let mut map = std::collections::HashMap::new();
     map.insert("a", "first value");
     map.insert("b", "second value");
     map.insert("c", "third value");
@@ -94,24 +100,47 @@ fn test_settings_macro() {
 #[test]
 fn test_snapshot_path() {
     with_settings!({snapshot_path => "snapshots2"}, {
-        assert_yaml_snapshot!(vec![1, 2, 3]);
+        assert_debug_snapshot!(vec![1, 2, 3]);
     });
 }
 
 #[test]
 fn test_snapshot_no_module_prepending() {
     with_settings!({prepend_module_to_snapshot => false}, {
-        assert_yaml_snapshot!(vec![1, 2, 3]);
+        assert_debug_snapshot!(vec![1, 2, 3]);
     });
 }
 
 #[test]
 fn test_snapshot_with_description() {
-    with_settings!({description => "The snapshot are three integers"}, {
-        assert_yaml_snapshot!(vec![1, 2, 3])
+    with_settings!({description => "The snapshot is three integers"}, {
+        assert_debug_snapshot!(vec![1, 2, 3])
+    });
+}
+
+#[test]
+fn test_snapshot_with_description_and_raw_info() {
+    use insta::internals::Content;
+
+    let raw_info = Content::Map(vec![
+        (
+            Content::from("env"),
+            Content::Seq(vec![
+                Content::from("ENVIRONMENT"),
+                Content::from("production"),
+            ]),
+        ),
+        (
+            Content::from("cmdline"),
+            Content::Seq(vec![Content::from("my-tool"), Content::from("run")]),
+        ),
+    ]);
+    with_settings!({description => "The snapshot is four integers", raw_info => &raw_info}, {
+        assert_debug_snapshot!(vec![1, 2, 3, 4])
     });
 }
 
+#[cfg(feature = "serialization")]
 #[test]
 fn test_snapshot_with_description_and_info() {
     #[derive(serde::Serialize)]
@@ -123,8 +152,8 @@ fn test_snapshot_with_description_and_info() {
         env: From::from([("ENVIRONMENT", "production")]),
         cmdline: vec!["my-tool", "run"],
     };
-    with_settings!({description => "The snapshot are four integers", info => &info}, {
-        assert_yaml_snapshot!(vec![1, 2, 3, 4])
+    with_settings!({description => "The snapshot is four integers", info => &info}, {
+        assert_debug_snapshot!(vec![1, 2, 3, 4])
     });
 }
 
diff --git a/tests/test_suffixes.rs b/tests/test_suffixes.rs
index c9c9f38..af0eb91 100644
--- a/tests/test_suffixes.rs
+++ b/tests/test_suffixes.rs
@@ -1,3 +1,4 @@
+#[cfg(feature = "json")]
 #[test]
 fn test_basic_suffixes() {
     for value in [1, 2, 3] {