diff --git a/crates/snapbox/src/data.rs b/crates/snapbox/src/data.rs
deleted file mode 100644
index 4e6107b4..00000000
--- a/crates/snapbox/src/data.rs
+++ /dev/null
@@ -1,1110 +0,0 @@
-/// Declare an expected value for an assert from a file
-///
-/// This is relative to the source file the macro is run from
-///
-/// ```
-/// # #[cfg(feature = "json")] {
-/// # use snapbox::file;
-/// file!["./test_data/bar.json"];
-/// file!["./test_data/bar.json": Text];  // do textual rather than structural comparisons
-/// file![_];
-/// file![_: Json];  // ensure its treated as json since a type can't be inferred
-/// # }
-/// ```
-#[macro_export]
-macro_rules! file {
-    [_] => {{
-        let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap();
-        let rel_path = ::std::format!("snapshots/{}-{}.txt", stem.to_str().unwrap(), line!());
-        let mut path = $crate::current_dir!();
-        path.push(rel_path);
-        $crate::Data::read_from(&path, None)
-    }};
-    [_ : $type:ident] => {{
-        let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap();
-        let ext = $crate::DataFormat:: $type.ext();
-        let rel_path = ::std::format!("snapshots/{}-{}.{ext}", stem.to_str().unwrap(), line!());
-        let mut path = $crate::current_dir!();
-        path.push(rel_path);
-        $crate::Data::read_from(&path, Some($crate::DataFormat:: $type))
-    }};
-    [$path:literal] => {{
-        let mut path = $crate::current_dir!();
-        path.push($path);
-        $crate::Data::read_from(&path, None)
-    }};
-    [$path:literal : $type:ident] => {{
-        let mut path = $crate::current_dir!();
-        path.push($path);
-        $crate::Data::read_from(&path, Some($crate::DataFormat:: $type))
-    }};
-}
-
-/// Test fixture, actual output, or expected result
-///
-/// This provides conveniences for tracking the intended format (binary vs text).
-#[derive(Clone, Debug)]
-pub struct Data {
-    inner: DataInner,
-    source: Option<DataSource>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-enum DataInner {
-    Error(crate::Error),
-    Binary(Vec<u8>),
-    Text(String),
-    #[cfg(feature = "json")]
-    Json(serde_json::Value),
-}
-
-impl Data {
-    /// Mark the data as binary (no post-processing)
-    pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
-        Self {
-            inner: DataInner::Binary(raw.into()),
-            source: None,
-        }
-    }
-
-    /// Mark the data as text (post-processing)
-    pub fn text(raw: impl Into<String>) -> Self {
-        Self {
-            inner: DataInner::Text(raw.into()),
-            source: None,
-        }
-    }
-
-    #[cfg(feature = "json")]
-    pub fn json(raw: impl Into<serde_json::Value>) -> Self {
-        Self {
-            inner: DataInner::Json(raw.into()),
-            source: None,
-        }
-    }
-
-    fn error(raw: impl Into<crate::Error>) -> Self {
-        Self {
-            inner: DataInner::Error(raw.into()),
-            source: None,
-        }
-    }
-
-    /// Empty test data
-    pub fn new() -> Self {
-        Self::text("")
-    }
-
-    fn with_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
-        self.source = Some(DataSource::path(path));
-        self
-    }
-
-    /// Load test data from a file
-    pub fn read_from(path: &std::path::Path, data_format: Option<DataFormat>) -> Self {
-        match Self::try_read_from(path, data_format) {
-            Ok(data) => data,
-            Err(err) => Self::error(err),
-        }
-    }
-
-    /// Load test data from a file
-    pub fn try_read_from(
-        path: &std::path::Path,
-        data_format: Option<DataFormat>,
-    ) -> Result<Self, crate::Error> {
-        let data = match data_format {
-            Some(df) => match df {
-                DataFormat::Error => Self::error("unknown error"),
-                DataFormat::Binary => {
-                    let data = std::fs::read(path)
-                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
-                    Self::binary(data)
-                }
-                DataFormat::Text => {
-                    let data = std::fs::read_to_string(path)
-                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
-                    Self::text(data)
-                }
-                #[cfg(feature = "json")]
-                DataFormat::Json => {
-                    let data = std::fs::read_to_string(path)
-                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
-                    Self::json(serde_json::from_str::<serde_json::Value>(&data).unwrap())
-                }
-            },
-            None => {
-                let data = std::fs::read(path)
-                    .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
-                let data = Self::binary(data);
-                match path
-                    .extension()
-                    .and_then(|e| e.to_str())
-                    .unwrap_or_default()
-                {
-                    #[cfg(feature = "json")]
-                    "json" => data.try_coerce(DataFormat::Json),
-                    _ => data.try_coerce(DataFormat::Text),
-                }
-            }
-        };
-        Ok(data.with_path(path))
-    }
-
-    /// Location the data came from
-    pub fn source(&self) -> Option<&DataSource> {
-        self.source.as_ref()
-    }
-
-    /// Overwrite a snapshot
-    pub fn write_to(&self, source: &DataSource) -> Result<(), crate::Error> {
-        match &source.inner {
-            DataSourceInner::Path(p) => self.write_to_path(p),
-        }
-    }
-
-    /// Overwrite a snapshot
-    pub fn write_to_path(&self, path: &std::path::Path) -> Result<(), crate::Error> {
-        if let Some(parent) = path.parent() {
-            std::fs::create_dir_all(parent).map_err(|e| {
-                format!("Failed to create parent dir for {}: {}", path.display(), e)
-            })?;
-        }
-        let bytes = self.to_bytes()?;
-        std::fs::write(path, bytes)
-            .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
-    }
-
-    /// Post-process text
-    ///
-    /// See [utils][crate::utils]
-    pub fn normalize(self, op: impl Normalize) -> Self {
-        op.normalize(self)
-    }
-
-    /// Return the underlying `String`
-    ///
-    /// Note: this will not inspect binary data for being a valid `String`.
-    pub fn render(&self) -> Option<String> {
-        match &self.inner {
-            DataInner::Error(_) => None,
-            DataInner::Binary(_) => None,
-            DataInner::Text(data) => Some(data.to_owned()),
-            #[cfg(feature = "json")]
-            DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()),
-        }
-    }
-
-    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::Error> {
-        match &self.inner {
-            DataInner::Error(err) => Err(err.clone()),
-            DataInner::Binary(data) => Ok(data.clone()),
-            DataInner::Text(data) => Ok(data.clone().into_bytes()),
-            #[cfg(feature = "json")]
-            DataInner::Json(value) => {
-                serde_json::to_vec_pretty(value).map_err(|err| format!("{err}").into())
-            }
-        }
-    }
-
-    pub fn try_coerce(self, format: DataFormat) -> Self {
-        let mut data = match (self.inner, format) {
-            (DataInner::Error(inner), _) => Self::error(inner),
-            (inner, DataFormat::Error) => Self {
-                inner,
-                source: None,
-            },
-            (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner),
-            (DataInner::Text(inner), DataFormat::Text) => Self::text(inner),
-            #[cfg(feature = "json")]
-            (DataInner::Json(inner), DataFormat::Json) => Self::json(inner),
-            (DataInner::Binary(inner), _) => {
-                if is_binary(&inner) {
-                    Self::binary(inner)
-                } else {
-                    match String::from_utf8(inner) {
-                        Ok(str) => {
-                            let coerced = Self::text(str).try_coerce(format);
-                            // if the Text cannot be coerced into the correct format
-                            // reset it back to Binary
-                            if coerced.format() != format {
-                                coerced.try_coerce(DataFormat::Binary)
-                            } else {
-                                coerced
-                            }
-                        }
-                        Err(err) => {
-                            let bin = err.into_bytes();
-                            Self::binary(bin)
-                        }
-                    }
-                }
-            }
-            #[cfg(feature = "json")]
-            (DataInner::Text(inner), DataFormat::Json) => {
-                match serde_json::from_str::<serde_json::Value>(&inner) {
-                    Ok(json) => Self::json(json),
-                    Err(_) => Self::text(inner),
-                }
-            }
-            (inner, DataFormat::Binary) => Self::binary(
-                Self {
-                    inner,
-                    source: None,
-                }
-                .to_bytes()
-                .expect("error case handled"),
-            ),
-            // This variant is already covered unless structured data is enabled
-            #[cfg(feature = "structured-data")]
-            (inner, DataFormat::Text) => {
-                let remake = Self {
-                    inner,
-                    source: None,
-                };
-                if let Some(str) = remake.render() {
-                    Self::text(str)
-                } else {
-                    remake
-                }
-            }
-        };
-        data.source = self.source;
-        data
-    }
-
-    /// Outputs the current `DataFormat` of the underlying data
-    pub fn format(&self) -> DataFormat {
-        match &self.inner {
-            DataInner::Error(_) => DataFormat::Error,
-            DataInner::Binary(_) => DataFormat::Binary,
-            DataInner::Text(_) => DataFormat::Text,
-            #[cfg(feature = "json")]
-            DataInner::Json(_) => DataFormat::Json,
-        }
-    }
-}
-
-impl std::fmt::Display for Data {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match &self.inner {
-            DataInner::Error(err) => err.fmt(f),
-            DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f),
-            DataInner::Text(data) => data.fmt(f),
-            #[cfg(feature = "json")]
-            DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
-        }
-    }
-}
-
-impl PartialEq for Data {
-    fn eq(&self, other: &Data) -> bool {
-        self.inner == other.inner
-    }
-}
-
-impl Eq for Data {}
-
-impl Default for Data {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl<'d> From<&'d Data> for Data {
-    fn from(other: &'d Data) -> Self {
-        other.clone()
-    }
-}
-
-impl From<Vec<u8>> for Data {
-    fn from(other: Vec<u8>) -> Self {
-        Self::binary(other)
-    }
-}
-
-impl<'b> From<&'b [u8]> for Data {
-    fn from(other: &'b [u8]) -> Self {
-        other.to_owned().into()
-    }
-}
-
-impl From<String> for Data {
-    fn from(other: String) -> Self {
-        Self::text(other)
-    }
-}
-
-impl<'s> From<&'s String> for Data {
-    fn from(other: &'s String) -> Self {
-        other.clone().into()
-    }
-}
-
-impl<'s> From<&'s str> for Data {
-    fn from(other: &'s str) -> Self {
-        other.to_owned().into()
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)]
-pub enum DataFormat {
-    Error,
-    Binary,
-    #[default]
-    Text,
-    #[cfg(feature = "json")]
-    Json,
-}
-
-impl DataFormat {
-    pub fn ext(self) -> &'static str {
-        match self {
-            Self::Error => "txt",
-            Self::Binary => "bin",
-            Self::Text => "txt",
-            #[cfg(feature = "json")]
-            Self::Json => "json",
-        }
-    }
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct DataSource {
-    inner: DataSourceInner,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-enum DataSourceInner {
-    Path(std::path::PathBuf),
-}
-
-impl DataSource {
-    pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
-        Self {
-            inner: DataSourceInner::Path(path.into()),
-        }
-    }
-
-    pub fn is_path(&self) -> bool {
-        self.as_path().is_some()
-    }
-
-    pub fn as_path(&self) -> Option<&std::path::Path> {
-        match &self.inner {
-            DataSourceInner::Path(path) => Some(path.as_ref()),
-        }
-    }
-}
-
-impl std::fmt::Display for DataSource {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match &self.inner {
-            DataSourceInner::Path(path) => crate::path::display_relpath(path).fmt(f),
-        }
-    }
-}
-
-pub trait Normalize {
-    fn normalize(&self, data: Data) -> Data;
-}
-
-pub struct NormalizeNewlines;
-impl Normalize for NormalizeNewlines {
-    fn normalize(&self, data: Data) -> Data {
-        let mut new = match data.inner {
-            DataInner::Error(err) => Data::error(err),
-            DataInner::Binary(bin) => Data::binary(bin),
-            DataInner::Text(text) => {
-                let lines = crate::utils::normalize_lines(&text);
-                Data::text(lines)
-            }
-            #[cfg(feature = "json")]
-            DataInner::Json(value) => {
-                let mut value = value;
-                normalize_value(&mut value, crate::utils::normalize_lines);
-                Data::json(value)
-            }
-        };
-        new.source = data.source;
-        new
-    }
-}
-
-pub struct NormalizePaths;
-impl Normalize for NormalizePaths {
-    fn normalize(&self, data: Data) -> Data {
-        let mut new = match data.inner {
-            DataInner::Error(err) => Data::error(err),
-            DataInner::Binary(bin) => Data::binary(bin),
-            DataInner::Text(text) => {
-                let lines = crate::utils::normalize_paths(&text);
-                Data::text(lines)
-            }
-            #[cfg(feature = "json")]
-            DataInner::Json(value) => {
-                let mut value = value;
-                normalize_value(&mut value, crate::utils::normalize_paths);
-                Data::json(value)
-            }
-        };
-        new.source = data.source;
-        new
-    }
-}
-
-pub struct NormalizeMatches<'a> {
-    substitutions: &'a crate::Substitutions,
-    pattern: &'a Data,
-}
-
-impl<'a> NormalizeMatches<'a> {
-    pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self {
-        NormalizeMatches {
-            substitutions,
-            pattern,
-        }
-    }
-}
-
-impl Normalize for NormalizeMatches<'_> {
-    fn normalize(&self, data: Data) -> Data {
-        let mut new = match data.inner {
-            DataInner::Error(err) => Data::error(err),
-            DataInner::Binary(bin) => Data::binary(bin),
-            DataInner::Text(text) => {
-                let lines = self
-                    .substitutions
-                    .normalize(&text, &self.pattern.render().unwrap());
-                Data::text(lines)
-            }
-            #[cfg(feature = "json")]
-            DataInner::Json(value) => {
-                let mut value = value;
-                if let DataInner::Json(exp) = &self.pattern.inner {
-                    normalize_value_matches(&mut value, exp, self.substitutions);
-                }
-                Data::json(value)
-            }
-        };
-        new.source = data.source;
-        new
-    }
-}
-
-#[cfg(feature = "structured-data")]
-fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) {
-    match value {
-        serde_json::Value::String(str) => {
-            *str = op(str);
-        }
-        serde_json::Value::Array(arr) => {
-            for value in arr.iter_mut() {
-                normalize_value(value, op)
-            }
-        }
-        serde_json::Value::Object(obj) => {
-            for (_, value) in obj.iter_mut() {
-                normalize_value(value, op)
-            }
-        }
-        _ => {}
-    }
-}
-
-#[cfg(feature = "structured-data")]
-fn normalize_value_matches(
-    actual: &mut serde_json::Value,
-    expected: &serde_json::Value,
-    substitutions: &crate::Substitutions,
-) {
-    use serde_json::Value::*;
-    match (actual, expected) {
-        // "{...}" is a wildcard
-        (act, String(exp)) if exp == "{...}" => {
-            *act = serde_json::json!("{...}");
-        }
-        (String(act), String(exp)) => {
-            *act = substitutions.normalize(act, exp);
-        }
-        (Array(act), Array(exp)) => {
-            let wildcard = String("{...}".to_string());
-            let mut sections = exp.split(|e| e == &wildcard).peekable();
-            let mut processed = 0;
-            while let Some(expected_subset) = sections.next() {
-                // Process all values in the current section
-                if !expected_subset.is_empty() {
-                    let actual_subset = &mut act[processed..processed + expected_subset.len()];
-                    for (a, e) in actual_subset.iter_mut().zip(expected_subset) {
-                        normalize_value_matches(a, e, substitutions);
-                    }
-                    processed += expected_subset.len();
-                }
-
-                if let Some(next_section) = sections.peek() {
-                    // If the next section has nothing in it, replace from processed to end with
-                    // a single "{...}"
-                    if next_section.is_empty() {
-                        act.splice(processed.., vec![wildcard.clone()]);
-                        processed += 1;
-                    } else {
-                        let first = next_section.first().unwrap();
-                        // Replace everything up until the value we are looking for with
-                        // a single "{...}".
-                        if let Some(index) = act.iter().position(|v| v == first) {
-                            act.splice(processed..index, vec![wildcard.clone()]);
-                            processed += 1;
-                        } else {
-                            // If we cannot find the value we are looking for return early
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-        (Object(act), Object(exp)) => {
-            for (a, e) in act.iter_mut().zip(exp).filter(|(a, e)| a.0 == e.0) {
-                normalize_value_matches(a.1, e.1, substitutions)
-            }
-        }
-        (_, _) => {}
-    }
-}
-
-#[cfg(feature = "detect-encoding")]
-fn is_binary(data: &[u8]) -> bool {
-    match content_inspector::inspect(data) {
-        content_inspector::ContentType::BINARY |
-        // We don't support these
-        content_inspector::ContentType::UTF_16LE |
-        content_inspector::ContentType::UTF_16BE |
-        content_inspector::ContentType::UTF_32LE |
-        content_inspector::ContentType::UTF_32BE => {
-            true
-        },
-        content_inspector::ContentType::UTF_8 |
-        content_inspector::ContentType::UTF_8_BOM => {
-            false
-        },
-    }
-}
-
-#[cfg(not(feature = "detect-encoding"))]
-fn is_binary(_data: &[u8]) -> bool {
-    false
-}
-
-#[cfg(test)]
-mod test {
-    use super::*;
-    #[cfg(feature = "json")]
-    use serde_json::json;
-
-    // Tests for checking to_bytes and render produce the same results
-    #[test]
-    fn text_to_bytes_render() {
-        let d = Data::text(String::from("test"));
-        let bytes = d.to_bytes().unwrap();
-        let bytes = String::from_utf8(bytes).unwrap();
-        let rendered = d.render().unwrap();
-        assert_eq!(bytes, rendered);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_to_bytes_render() {
-        let d = Data::json(json!({"name": "John\\Doe\r\n"}));
-        let bytes = d.to_bytes().unwrap();
-        let bytes = String::from_utf8(bytes).unwrap();
-        let rendered = d.render().unwrap();
-        assert_eq!(bytes, rendered);
-    }
-
-    // Tests for checking all types are coercible to each other and
-    // for when the coercion should fail
-    #[test]
-    fn binary_to_text() {
-        let binary = String::from("test").into_bytes();
-        let d = Data::binary(binary);
-        let text = d.try_coerce(DataFormat::Text);
-        assert_eq!(DataFormat::Text, text.format())
-    }
-
-    #[test]
-    fn binary_to_text_not_utf8() {
-        let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
-        let d = Data::binary(binary);
-        let d = d.try_coerce(DataFormat::Text);
-        assert_ne!(DataFormat::Text, d.format());
-        assert_eq!(DataFormat::Binary, d.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn binary_to_json() {
-        let value = json!({"name": "John\\Doe\r\n"});
-        let binary = serde_json::to_vec_pretty(&value).unwrap();
-        let d = Data::binary(binary);
-        let json = d.try_coerce(DataFormat::Json);
-        assert_eq!(DataFormat::Json, json.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn binary_to_json_not_utf8() {
-        let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
-        let d = Data::binary(binary);
-        let d = d.try_coerce(DataFormat::Json);
-        assert_ne!(DataFormat::Json, d.format());
-        assert_eq!(DataFormat::Binary, d.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn binary_to_json_not_json() {
-        let binary = String::from("test").into_bytes();
-        let d = Data::binary(binary);
-        let d = d.try_coerce(DataFormat::Json);
-        assert_ne!(DataFormat::Json, d.format());
-        assert_eq!(DataFormat::Binary, d.format());
-    }
-
-    #[test]
-    fn text_to_binary() {
-        let text = String::from("test");
-        let d = Data::text(text);
-        let binary = d.try_coerce(DataFormat::Binary);
-        assert_eq!(DataFormat::Binary, binary.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn text_to_json() {
-        let value = json!({"name": "John\\Doe\r\n"});
-        let text = serde_json::to_string_pretty(&value).unwrap();
-        let d = Data::text(text);
-        let json = d.try_coerce(DataFormat::Json);
-        assert_eq!(DataFormat::Json, json.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn text_to_json_not_json() {
-        let text = String::from("test");
-        let d = Data::text(text);
-        let json = d.try_coerce(DataFormat::Json);
-        assert_eq!(DataFormat::Text, json.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_to_binary() {
-        let value = json!({"name": "John\\Doe\r\n"});
-        let d = Data::json(value);
-        let binary = d.try_coerce(DataFormat::Binary);
-        assert_eq!(DataFormat::Binary, binary.format());
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_to_text() {
-        let value = json!({"name": "John\\Doe\r\n"});
-        let d = Data::json(value);
-        let text = d.try_coerce(DataFormat::Text);
-        assert_eq!(DataFormat::Text, text.format());
-    }
-
-    // Tests for coercible conversions create the same output as to_bytes/render
-    //
-    // render does not need to be checked against bin -> text since render
-    // outputs None for binary
-    #[test]
-    fn text_to_bin_coerce_equals_to_bytes() {
-        let text = String::from("test");
-        let d = Data::text(text);
-        let binary = d.clone().try_coerce(DataFormat::Binary);
-        assert_eq!(Data::binary(d.to_bytes().unwrap()), binary);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_to_bin_coerce_equals_to_bytes() {
-        let json = json!({"name": "John\\Doe\r\n"});
-        let d = Data::json(json);
-        let binary = d.clone().try_coerce(DataFormat::Binary);
-        assert_eq!(Data::binary(d.to_bytes().unwrap()), binary);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_to_text_coerce_equals_render() {
-        let json = json!({"name": "John\\Doe\r\n"});
-        let d = Data::json(json);
-        let text = d.clone().try_coerce(DataFormat::Text);
-        assert_eq!(Data::text(d.render().unwrap()), text);
-    }
-
-    // Tests for normalization on json
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_paths_and_lines() {
-        let json = json!({"name": "John\\Doe\r\n"});
-        let data = Data::json(json);
-        let data = data.normalize(NormalizePaths);
-        assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data);
-        let data = data.normalize(NormalizeNewlines);
-        assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_obj_paths_and_lines() {
-        let json = json!({
-            "person": {
-                "name": "John\\Doe\r\n",
-                "nickname": "Jo\\hn\r\n",
-            }
-        });
-        let data = Data::json(json);
-        let data = data.normalize(NormalizePaths);
-        let assert = json!({
-            "person": {
-                "name": "John/Doe\r\n",
-                "nickname": "Jo/hn\r\n",
-            }
-        });
-        assert_eq!(Data::json(assert), data);
-        let data = data.normalize(NormalizeNewlines);
-        let assert = json!({
-            "person": {
-                "name": "John/Doe\n",
-                "nickname": "Jo/hn\n",
-            }
-        });
-        assert_eq!(Data::json(assert), data);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_array_paths_and_lines() {
-        let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]});
-        let data = Data::json(json);
-        let data = data.normalize(NormalizePaths);
-        let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]});
-        assert_eq!(Data::json(paths), data);
-        let data = data.normalize(NormalizeNewlines);
-        let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]});
-        assert_eq!(Data::json(new_lines), data);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_array_obj_paths_and_lines() {
-        let json = json!({
-            "people": [
-                {
-                    "name": "John\\Doe\r\n",
-                    "nickname": "Jo\\hn\r\n",
-                }
-            ]
-        });
-        let data = Data::json(json);
-        let data = data.normalize(NormalizePaths);
-        let paths = json!({
-            "people": [
-                {
-                    "name": "John/Doe\r\n",
-                    "nickname": "Jo/hn\r\n",
-                }
-            ]
-        });
-        assert_eq!(Data::json(paths), data);
-        let data = data.normalize(NormalizeNewlines);
-        let new_lines = json!({
-            "people": [
-                {
-                    "name": "John/Doe\n",
-                    "nickname": "Jo/hn\n",
-                }
-            ]
-        });
-        assert_eq!(Data::json(new_lines), data);
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_matches_string() {
-        let exp = json!({"name": "{...}"});
-        let expected = Data::json(exp);
-        let actual = json!({"name": "JohnDoe"});
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_matches_array() {
-        let exp = json!({"people": "{...}"});
-        let expected = Data::json(exp);
-        let actual = json!({
-            "people": [
-                {
-                    "name": "JohnDoe",
-                    "nickname": "John",
-                }
-            ]
-        });
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_matches_obj() {
-        let exp = json!({"people": "{...}"});
-        let expected = Data::json(exp);
-        let actual = json!({
-            "people": {
-                "name": "JohnDoe",
-                "nickname": "John",
-            }
-        });
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_matches_diff_order_array() {
-        let exp = json!({
-            "people": ["John", "Jane"]
-        });
-        let expected = Data::json(exp);
-        let actual = json!({
-            "people": ["Jane", "John"]
-        });
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_ne!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_wildcard_object_first() {
-        let exp = json!({
-            "people": [
-                "{...}",
-                {
-                    "name": "three",
-                    "nickname": "3",
-                }
-            ]
-        });
-        let expected = Data::json(exp);
-        let actual = json!({
-            "people": [
-                {
-                    "name": "one",
-                    "nickname": "1",
-                },
-                {
-                    "name": "two",
-                    "nickname": "2",
-                },
-                {
-                    "name": "three",
-                    "nickname": "3",
-                }
-            ]
-        });
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_wildcard_array_first() {
-        let exp = json!([
-            "{...}",
-            {
-                "name": "three",
-                "nickname": "3",
-            }
-        ]);
-        let expected = Data::json(exp);
-        let actual = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            {
-                "name": "two",
-                "nickname": "2",
-            },
-            {
-                "name": "three",
-                "nickname": "3",
-            }
-        ]);
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_wildcard_array_first_last() {
-        let exp = json!([
-            "{...}",
-            {
-                "name": "two",
-                "nickname": "2",
-            },
-            "{...}"
-        ]);
-        let expected = Data::json(exp);
-        let actual = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            {
-                "name": "two",
-                "nickname": "2",
-            },
-            {
-                "name": "three",
-                "nickname": "3",
-            },
-            {
-                "name": "four",
-                "nickname": "4",
-            }
-        ]);
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_wildcard_array_middle_last() {
-        let exp = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            "{...}",
-            {
-                "name": "three",
-                "nickname": "3",
-            },
-            "{...}"
-        ]);
-        let expected = Data::json(exp);
-        let actual = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            {
-                "name": "two",
-                "nickname": "2",
-            },
-            {
-                "name": "three",
-                "nickname": "3",
-            },
-            {
-                "name": "four",
-                "nickname": "4",
-            },
-            {
-                "name": "five",
-                "nickname": "5",
-            }
-        ]);
-        let actual = Data::json(actual).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
-            assert_eq!(exp, act);
-        }
-    }
-
-    #[test]
-    #[cfg(feature = "json")]
-    fn json_normalize_wildcard_array_middle_last_early_return() {
-        let exp = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            "{...}",
-            {
-                "name": "three",
-                "nickname": "3",
-            },
-            "{...}"
-        ]);
-        let expected = Data::json(exp);
-        let actual = json!([
-            {
-                "name": "one",
-                "nickname": "1",
-            },
-            {
-                "name": "two",
-                "nickname": "2",
-            },
-            {
-                "name": "four",
-                "nickname": "4",
-            },
-            {
-                "name": "five",
-                "nickname": "5",
-            }
-        ]);
-        let actual_normalized = Data::json(actual.clone()).normalize(NormalizeMatches {
-            substitutions: &Default::default(),
-            pattern: &expected,
-        });
-        if let DataInner::Json(act) = actual_normalized.inner {
-            assert_eq!(act, actual);
-        }
-    }
-}
diff --git a/crates/snapbox/src/data/format.rs b/crates/snapbox/src/data/format.rs
new file mode 100644
index 00000000..3bfea284
--- /dev/null
+++ b/crates/snapbox/src/data/format.rs
@@ -0,0 +1,21 @@
+#[derive(Clone, Debug, PartialEq, Eq, Copy, Hash, Default)]
+pub enum DataFormat {
+    Error,
+    Binary,
+    #[default]
+    Text,
+    #[cfg(feature = "json")]
+    Json,
+}
+
+impl DataFormat {
+    pub fn ext(self) -> &'static str {
+        match self {
+            Self::Error => "txt",
+            Self::Binary => "bin",
+            Self::Text => "txt",
+            #[cfg(feature = "json")]
+            Self::Json => "json",
+        }
+    }
+}
diff --git a/crates/snapbox/src/data/mod.rs b/crates/snapbox/src/data/mod.rs
new file mode 100644
index 00000000..b2123c81
--- /dev/null
+++ b/crates/snapbox/src/data/mod.rs
@@ -0,0 +1,384 @@
+mod format;
+mod normalize;
+mod source;
+#[cfg(test)]
+mod tests;
+
+pub use format::DataFormat;
+pub use normalize::Normalize;
+pub use normalize::NormalizeMatches;
+pub use normalize::NormalizeNewlines;
+pub use normalize::NormalizePaths;
+pub use source::DataSource;
+
+/// Declare an expected value for an assert from a file
+///
+/// This is relative to the source file the macro is run from
+///
+/// ```
+/// # #[cfg(feature = "json")] {
+/// # use snapbox::file;
+/// file!["./test_data/bar.json"];
+/// file!["./test_data/bar.json": Text];  // do textual rather than structural comparisons
+/// file![_];
+/// file![_: Json];  // ensure its treated as json since a type can't be inferred
+/// # }
+/// ```
+#[macro_export]
+macro_rules! file {
+    [_] => {{
+        let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap();
+        let rel_path = ::std::format!("snapshots/{}-{}.txt", stem.to_str().unwrap(), line!());
+        let mut path = $crate::current_dir!();
+        path.push(rel_path);
+        $crate::Data::read_from(&path, None)
+    }};
+    [_ : $type:ident] => {{
+        let stem = ::std::path::Path::new(::std::file!()).file_stem().unwrap();
+        let ext = $crate::DataFormat:: $type.ext();
+        let rel_path = ::std::format!("snapshots/{}-{}.{ext}", stem.to_str().unwrap(), line!());
+        let mut path = $crate::current_dir!();
+        path.push(rel_path);
+        $crate::Data::read_from(&path, Some($crate::DataFormat:: $type))
+    }};
+    [$path:literal] => {{
+        let mut path = $crate::current_dir!();
+        path.push($path);
+        $crate::Data::read_from(&path, None)
+    }};
+    [$path:literal : $type:ident] => {{
+        let mut path = $crate::current_dir!();
+        path.push($path);
+        $crate::Data::read_from(&path, Some($crate::DataFormat:: $type))
+    }};
+}
+
+/// Test fixture, actual output, or expected result
+///
+/// This provides conveniences for tracking the intended format (binary vs text).
+#[derive(Clone, Debug)]
+pub struct Data {
+    inner: DataInner,
+    source: Option<DataSource>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum DataInner {
+    Error(crate::Error),
+    Binary(Vec<u8>),
+    Text(String),
+    #[cfg(feature = "json")]
+    Json(serde_json::Value),
+}
+
+impl Data {
+    /// Mark the data as binary (no post-processing)
+    pub fn binary(raw: impl Into<Vec<u8>>) -> Self {
+        Self {
+            inner: DataInner::Binary(raw.into()),
+            source: None,
+        }
+    }
+
+    /// Mark the data as text (post-processing)
+    pub fn text(raw: impl Into<String>) -> Self {
+        Self {
+            inner: DataInner::Text(raw.into()),
+            source: None,
+        }
+    }
+
+    #[cfg(feature = "json")]
+    pub fn json(raw: impl Into<serde_json::Value>) -> Self {
+        Self {
+            inner: DataInner::Json(raw.into()),
+            source: None,
+        }
+    }
+
+    fn error(raw: impl Into<crate::Error>) -> Self {
+        Self {
+            inner: DataInner::Error(raw.into()),
+            source: None,
+        }
+    }
+
+    /// Empty test data
+    pub fn new() -> Self {
+        Self::text("")
+    }
+
+    fn with_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
+        self.source = Some(DataSource::path(path));
+        self
+    }
+
+    /// Load test data from a file
+    pub fn read_from(path: &std::path::Path, data_format: Option<DataFormat>) -> Self {
+        match Self::try_read_from(path, data_format) {
+            Ok(data) => data,
+            Err(err) => Self::error(err),
+        }
+    }
+
+    /// Load test data from a file
+    pub fn try_read_from(
+        path: &std::path::Path,
+        data_format: Option<DataFormat>,
+    ) -> Result<Self, crate::Error> {
+        let data = match data_format {
+            Some(df) => match df {
+                DataFormat::Error => Self::error("unknown error"),
+                DataFormat::Binary => {
+                    let data = std::fs::read(path)
+                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+                    Self::binary(data)
+                }
+                DataFormat::Text => {
+                    let data = std::fs::read_to_string(path)
+                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+                    Self::text(data)
+                }
+                #[cfg(feature = "json")]
+                DataFormat::Json => {
+                    let data = std::fs::read_to_string(path)
+                        .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+                    Self::json(serde_json::from_str::<serde_json::Value>(&data).unwrap())
+                }
+            },
+            None => {
+                let data = std::fs::read(path)
+                    .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
+                let data = Self::binary(data);
+                match path
+                    .extension()
+                    .and_then(|e| e.to_str())
+                    .unwrap_or_default()
+                {
+                    #[cfg(feature = "json")]
+                    "json" => data.try_coerce(DataFormat::Json),
+                    _ => data.try_coerce(DataFormat::Text),
+                }
+            }
+        };
+        Ok(data.with_path(path))
+    }
+
+    /// Location the data came from
+    pub fn source(&self) -> Option<&DataSource> {
+        self.source.as_ref()
+    }
+
+    /// Overwrite a snapshot
+    pub fn write_to(&self, source: &DataSource) -> Result<(), crate::Error> {
+        match &source.inner {
+            source::DataSourceInner::Path(p) => self.write_to_path(p),
+        }
+    }
+
+    /// Overwrite a snapshot
+    pub fn write_to_path(&self, path: &std::path::Path) -> Result<(), crate::Error> {
+        if let Some(parent) = path.parent() {
+            std::fs::create_dir_all(parent).map_err(|e| {
+                format!("Failed to create parent dir for {}: {}", path.display(), e)
+            })?;
+        }
+        let bytes = self.to_bytes()?;
+        std::fs::write(path, bytes)
+            .map_err(|e| format!("Failed to write {}: {}", path.display(), e).into())
+    }
+
+    /// Post-process text
+    ///
+    /// See [utils][crate::utils]
+    pub fn normalize(self, op: impl Normalize) -> Self {
+        op.normalize(self)
+    }
+
+    /// Return the underlying `String`
+    ///
+    /// Note: this will not inspect binary data for being a valid `String`.
+    pub fn render(&self) -> Option<String> {
+        match &self.inner {
+            DataInner::Error(_) => None,
+            DataInner::Binary(_) => None,
+            DataInner::Text(data) => Some(data.to_owned()),
+            #[cfg(feature = "json")]
+            DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()),
+        }
+    }
+
+    pub fn to_bytes(&self) -> Result<Vec<u8>, crate::Error> {
+        match &self.inner {
+            DataInner::Error(err) => Err(err.clone()),
+            DataInner::Binary(data) => Ok(data.clone()),
+            DataInner::Text(data) => Ok(data.clone().into_bytes()),
+            #[cfg(feature = "json")]
+            DataInner::Json(value) => {
+                serde_json::to_vec_pretty(value).map_err(|err| format!("{err}").into())
+            }
+        }
+    }
+
+    pub fn try_coerce(self, format: DataFormat) -> Self {
+        let mut data = match (self.inner, format) {
+            (DataInner::Error(inner), _) => Self::error(inner),
+            (inner, DataFormat::Error) => Self {
+                inner,
+                source: None,
+            },
+            (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner),
+            (DataInner::Text(inner), DataFormat::Text) => Self::text(inner),
+            #[cfg(feature = "json")]
+            (DataInner::Json(inner), DataFormat::Json) => Self::json(inner),
+            (DataInner::Binary(inner), _) => {
+                if is_binary(&inner) {
+                    Self::binary(inner)
+                } else {
+                    match String::from_utf8(inner) {
+                        Ok(str) => {
+                            let coerced = Self::text(str).try_coerce(format);
+                            // if the Text cannot be coerced into the correct format
+                            // reset it back to Binary
+                            if coerced.format() != format {
+                                coerced.try_coerce(DataFormat::Binary)
+                            } else {
+                                coerced
+                            }
+                        }
+                        Err(err) => {
+                            let bin = err.into_bytes();
+                            Self::binary(bin)
+                        }
+                    }
+                }
+            }
+            #[cfg(feature = "json")]
+            (DataInner::Text(inner), DataFormat::Json) => {
+                match serde_json::from_str::<serde_json::Value>(&inner) {
+                    Ok(json) => Self::json(json),
+                    Err(_) => Self::text(inner),
+                }
+            }
+            (inner, DataFormat::Binary) => Self::binary(
+                Self {
+                    inner,
+                    source: None,
+                }
+                .to_bytes()
+                .expect("error case handled"),
+            ),
+            // This variant is already covered unless structured data is enabled
+            #[cfg(feature = "structured-data")]
+            (inner, DataFormat::Text) => {
+                let remake = Self {
+                    inner,
+                    source: None,
+                };
+                if let Some(str) = remake.render() {
+                    Self::text(str)
+                } else {
+                    remake
+                }
+            }
+        };
+        data.source = self.source;
+        data
+    }
+
+    /// Outputs the current `DataFormat` of the underlying data
+    pub fn format(&self) -> DataFormat {
+        match &self.inner {
+            DataInner::Error(_) => DataFormat::Error,
+            DataInner::Binary(_) => DataFormat::Binary,
+            DataInner::Text(_) => DataFormat::Text,
+            #[cfg(feature = "json")]
+            DataInner::Json(_) => DataFormat::Json,
+        }
+    }
+}
+
+impl std::fmt::Display for Data {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self.inner {
+            DataInner::Error(err) => err.fmt(f),
+            DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f),
+            DataInner::Text(data) => data.fmt(f),
+            #[cfg(feature = "json")]
+            DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
+        }
+    }
+}
+
+impl PartialEq for Data {
+    fn eq(&self, other: &Data) -> bool {
+        self.inner == other.inner
+    }
+}
+
+impl Eq for Data {}
+
+impl Default for Data {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl<'d> From<&'d Data> for Data {
+    fn from(other: &'d Data) -> Self {
+        other.clone()
+    }
+}
+
+impl From<Vec<u8>> for Data {
+    fn from(other: Vec<u8>) -> Self {
+        Self::binary(other)
+    }
+}
+
+impl<'b> From<&'b [u8]> for Data {
+    fn from(other: &'b [u8]) -> Self {
+        other.to_owned().into()
+    }
+}
+
+impl From<String> for Data {
+    fn from(other: String) -> Self {
+        Self::text(other)
+    }
+}
+
+impl<'s> From<&'s String> for Data {
+    fn from(other: &'s String) -> Self {
+        other.clone().into()
+    }
+}
+
+impl<'s> From<&'s str> for Data {
+    fn from(other: &'s str) -> Self {
+        other.to_owned().into()
+    }
+}
+
+#[cfg(feature = "detect-encoding")]
+fn is_binary(data: &[u8]) -> bool {
+    match content_inspector::inspect(data) {
+        content_inspector::ContentType::BINARY |
+        // We don't support these
+        content_inspector::ContentType::UTF_16LE |
+        content_inspector::ContentType::UTF_16BE |
+        content_inspector::ContentType::UTF_32LE |
+        content_inspector::ContentType::UTF_32BE => {
+            true
+        },
+        content_inspector::ContentType::UTF_8 |
+        content_inspector::ContentType::UTF_8_BOM => {
+            false
+        },
+    }
+}
+
+#[cfg(not(feature = "detect-encoding"))]
+fn is_binary(_data: &[u8]) -> bool {
+    false
+}
diff --git a/crates/snapbox/src/data/normalize.rs b/crates/snapbox/src/data/normalize.rs
new file mode 100644
index 00000000..316bcba7
--- /dev/null
+++ b/crates/snapbox/src/data/normalize.rs
@@ -0,0 +1,168 @@
+use super::Data;
+use super::DataInner;
+
+pub trait Normalize {
+    fn normalize(&self, data: Data) -> Data;
+}
+
+pub struct NormalizeNewlines;
+impl Normalize for NormalizeNewlines {
+    fn normalize(&self, data: Data) -> Data {
+        let mut new = match data.inner {
+            DataInner::Error(err) => Data::error(err),
+            DataInner::Binary(bin) => Data::binary(bin),
+            DataInner::Text(text) => {
+                let lines = crate::utils::normalize_lines(&text);
+                Data::text(lines)
+            }
+            #[cfg(feature = "json")]
+            DataInner::Json(value) => {
+                let mut value = value;
+                normalize_value(&mut value, crate::utils::normalize_lines);
+                Data::json(value)
+            }
+        };
+        new.source = data.source;
+        new
+    }
+}
+
+pub struct NormalizePaths;
+impl Normalize for NormalizePaths {
+    fn normalize(&self, data: Data) -> Data {
+        let mut new = match data.inner {
+            DataInner::Error(err) => Data::error(err),
+            DataInner::Binary(bin) => Data::binary(bin),
+            DataInner::Text(text) => {
+                let lines = crate::utils::normalize_paths(&text);
+                Data::text(lines)
+            }
+            #[cfg(feature = "json")]
+            DataInner::Json(value) => {
+                let mut value = value;
+                normalize_value(&mut value, crate::utils::normalize_paths);
+                Data::json(value)
+            }
+        };
+        new.source = data.source;
+        new
+    }
+}
+
+pub struct NormalizeMatches<'a> {
+    substitutions: &'a crate::Substitutions,
+    pattern: &'a Data,
+}
+
+impl<'a> NormalizeMatches<'a> {
+    pub fn new(substitutions: &'a crate::Substitutions, pattern: &'a Data) -> Self {
+        NormalizeMatches {
+            substitutions,
+            pattern,
+        }
+    }
+}
+
+impl Normalize for NormalizeMatches<'_> {
+    fn normalize(&self, data: Data) -> Data {
+        let mut new = match data.inner {
+            DataInner::Error(err) => Data::error(err),
+            DataInner::Binary(bin) => Data::binary(bin),
+            DataInner::Text(text) => {
+                let lines = self
+                    .substitutions
+                    .normalize(&text, &self.pattern.render().unwrap());
+                Data::text(lines)
+            }
+            #[cfg(feature = "json")]
+            DataInner::Json(value) => {
+                let mut value = value;
+                if let DataInner::Json(exp) = &self.pattern.inner {
+                    normalize_value_matches(&mut value, exp, self.substitutions);
+                }
+                Data::json(value)
+            }
+        };
+        new.source = data.source;
+        new
+    }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) {
+    match value {
+        serde_json::Value::String(str) => {
+            *str = op(str);
+        }
+        serde_json::Value::Array(arr) => {
+            for value in arr.iter_mut() {
+                normalize_value(value, op)
+            }
+        }
+        serde_json::Value::Object(obj) => {
+            for (_, value) in obj.iter_mut() {
+                normalize_value(value, op)
+            }
+        }
+        _ => {}
+    }
+}
+
+#[cfg(feature = "structured-data")]
+fn normalize_value_matches(
+    actual: &mut serde_json::Value,
+    expected: &serde_json::Value,
+    substitutions: &crate::Substitutions,
+) {
+    use serde_json::Value::*;
+    match (actual, expected) {
+        // "{...}" is a wildcard
+        (act, String(exp)) if exp == "{...}" => {
+            *act = serde_json::json!("{...}");
+        }
+        (String(act), String(exp)) => {
+            *act = substitutions.normalize(act, exp);
+        }
+        (Array(act), Array(exp)) => {
+            let wildcard = String("{...}".to_string());
+            let mut sections = exp.split(|e| e == &wildcard).peekable();
+            let mut processed = 0;
+            while let Some(expected_subset) = sections.next() {
+                // Process all values in the current section
+                if !expected_subset.is_empty() {
+                    let actual_subset = &mut act[processed..processed + expected_subset.len()];
+                    for (a, e) in actual_subset.iter_mut().zip(expected_subset) {
+                        normalize_value_matches(a, e, substitutions);
+                    }
+                    processed += expected_subset.len();
+                }
+
+                if let Some(next_section) = sections.peek() {
+                    // If the next section has nothing in it, replace from processed to end with
+                    // a single "{...}"
+                    if next_section.is_empty() {
+                        act.splice(processed.., vec![wildcard.clone()]);
+                        processed += 1;
+                    } else {
+                        let first = next_section.first().unwrap();
+                        // Replace everything up until the value we are looking for with
+                        // a single "{...}".
+                        if let Some(index) = act.iter().position(|v| v == first) {
+                            act.splice(processed..index, vec![wildcard.clone()]);
+                            processed += 1;
+                        } else {
+                            // If we cannot find the value we are looking for return early
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        (Object(act), Object(exp)) => {
+            for (a, e) in act.iter_mut().zip(exp).filter(|(a, e)| a.0 == e.0) {
+                normalize_value_matches(a.1, e.1, substitutions)
+            }
+        }
+        (_, _) => {}
+    }
+}
diff --git a/crates/snapbox/src/data/source.rs b/crates/snapbox/src/data/source.rs
new file mode 100644
index 00000000..1d57518b
--- /dev/null
+++ b/crates/snapbox/src/data/source.rs
@@ -0,0 +1,35 @@
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct DataSource {
+    pub(crate) inner: DataSourceInner,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub(crate) enum DataSourceInner {
+    Path(std::path::PathBuf),
+}
+
+impl DataSource {
+    pub fn path(path: impl Into<std::path::PathBuf>) -> Self {
+        Self {
+            inner: DataSourceInner::Path(path.into()),
+        }
+    }
+
+    pub fn is_path(&self) -> bool {
+        self.as_path().is_some()
+    }
+
+    pub fn as_path(&self) -> Option<&std::path::Path> {
+        match &self.inner {
+            DataSourceInner::Path(path) => Some(path.as_ref()),
+        }
+    }
+}
+
+impl std::fmt::Display for DataSource {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match &self.inner {
+            DataSourceInner::Path(path) => crate::path::display_relpath(path).fmt(f),
+        }
+    }
+}
diff --git a/crates/snapbox/src/data/tests.rs b/crates/snapbox/src/data/tests.rs
new file mode 100644
index 00000000..bd53707e
--- /dev/null
+++ b/crates/snapbox/src/data/tests.rs
@@ -0,0 +1,493 @@
+use super::*;
+#[cfg(feature = "json")]
+use serde_json::json;
+
+// Tests for checking to_bytes and render produce the same results
+#[test]
+fn text_to_bytes_render() {
+    let d = Data::text(String::from("test"));
+    let bytes = d.to_bytes().unwrap();
+    let bytes = String::from_utf8(bytes).unwrap();
+    let rendered = d.render().unwrap();
+    assert_eq!(bytes, rendered);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_to_bytes_render() {
+    let d = Data::json(json!({"name": "John\\Doe\r\n"}));
+    let bytes = d.to_bytes().unwrap();
+    let bytes = String::from_utf8(bytes).unwrap();
+    let rendered = d.render().unwrap();
+    assert_eq!(bytes, rendered);
+}
+
+// Tests for checking all types are coercible to each other and
+// for when the coercion should fail
+#[test]
+fn binary_to_text() {
+    let binary = String::from("test").into_bytes();
+    let d = Data::binary(binary);
+    let text = d.try_coerce(DataFormat::Text);
+    assert_eq!(DataFormat::Text, text.format())
+}
+
+#[test]
+fn binary_to_text_not_utf8() {
+    let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+    let d = Data::binary(binary);
+    let d = d.try_coerce(DataFormat::Text);
+    assert_ne!(DataFormat::Text, d.format());
+    assert_eq!(DataFormat::Binary, d.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn binary_to_json() {
+    let value = json!({"name": "John\\Doe\r\n"});
+    let binary = serde_json::to_vec_pretty(&value).unwrap();
+    let d = Data::binary(binary);
+    let json = d.try_coerce(DataFormat::Json);
+    assert_eq!(DataFormat::Json, json.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn binary_to_json_not_utf8() {
+    let binary = b"\xFF\xE0\x00\x10\x4A\x46\x49\x46\x00".to_vec();
+    let d = Data::binary(binary);
+    let d = d.try_coerce(DataFormat::Json);
+    assert_ne!(DataFormat::Json, d.format());
+    assert_eq!(DataFormat::Binary, d.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn binary_to_json_not_json() {
+    let binary = String::from("test").into_bytes();
+    let d = Data::binary(binary);
+    let d = d.try_coerce(DataFormat::Json);
+    assert_ne!(DataFormat::Json, d.format());
+    assert_eq!(DataFormat::Binary, d.format());
+}
+
+#[test]
+fn text_to_binary() {
+    let text = String::from("test");
+    let d = Data::text(text);
+    let binary = d.try_coerce(DataFormat::Binary);
+    assert_eq!(DataFormat::Binary, binary.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn text_to_json() {
+    let value = json!({"name": "John\\Doe\r\n"});
+    let text = serde_json::to_string_pretty(&value).unwrap();
+    let d = Data::text(text);
+    let json = d.try_coerce(DataFormat::Json);
+    assert_eq!(DataFormat::Json, json.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn text_to_json_not_json() {
+    let text = String::from("test");
+    let d = Data::text(text);
+    let json = d.try_coerce(DataFormat::Json);
+    assert_eq!(DataFormat::Text, json.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_to_binary() {
+    let value = json!({"name": "John\\Doe\r\n"});
+    let d = Data::json(value);
+    let binary = d.try_coerce(DataFormat::Binary);
+    assert_eq!(DataFormat::Binary, binary.format());
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_to_text() {
+    let value = json!({"name": "John\\Doe\r\n"});
+    let d = Data::json(value);
+    let text = d.try_coerce(DataFormat::Text);
+    assert_eq!(DataFormat::Text, text.format());
+}
+
+// Tests for coercible conversions create the same output as to_bytes/render
+//
+// render does not need to be checked against bin -> text since render
+// outputs None for binary
+#[test]
+fn text_to_bin_coerce_equals_to_bytes() {
+    let text = String::from("test");
+    let d = Data::text(text);
+    let binary = d.clone().try_coerce(DataFormat::Binary);
+    assert_eq!(Data::binary(d.to_bytes().unwrap()), binary);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_to_bin_coerce_equals_to_bytes() {
+    let json = json!({"name": "John\\Doe\r\n"});
+    let d = Data::json(json);
+    let binary = d.clone().try_coerce(DataFormat::Binary);
+    assert_eq!(Data::binary(d.to_bytes().unwrap()), binary);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_to_text_coerce_equals_render() {
+    let json = json!({"name": "John\\Doe\r\n"});
+    let d = Data::json(json);
+    let text = d.clone().try_coerce(DataFormat::Text);
+    assert_eq!(Data::text(d.render().unwrap()), text);
+}
+
+// Tests for normalization on json
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_paths_and_lines() {
+    let json = json!({"name": "John\\Doe\r\n"});
+    let data = Data::json(json);
+    let data = data.normalize(NormalizePaths);
+    assert_eq!(Data::json(json!({"name": "John/Doe\r\n"})), data);
+    let data = data.normalize(NormalizeNewlines);
+    assert_eq!(Data::json(json!({"name": "John/Doe\n"})), data);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_obj_paths_and_lines() {
+    let json = json!({
+        "person": {
+            "name": "John\\Doe\r\n",
+            "nickname": "Jo\\hn\r\n",
+        }
+    });
+    let data = Data::json(json);
+    let data = data.normalize(NormalizePaths);
+    let assert = json!({
+        "person": {
+            "name": "John/Doe\r\n",
+            "nickname": "Jo/hn\r\n",
+        }
+    });
+    assert_eq!(Data::json(assert), data);
+    let data = data.normalize(NormalizeNewlines);
+    let assert = json!({
+        "person": {
+            "name": "John/Doe\n",
+            "nickname": "Jo/hn\n",
+        }
+    });
+    assert_eq!(Data::json(assert), data);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_array_paths_and_lines() {
+    let json = json!({"people": ["John\\Doe\r\n", "Jo\\hn\r\n"]});
+    let data = Data::json(json);
+    let data = data.normalize(NormalizePaths);
+    let paths = json!({"people": ["John/Doe\r\n", "Jo/hn\r\n"]});
+    assert_eq!(Data::json(paths), data);
+    let data = data.normalize(NormalizeNewlines);
+    let new_lines = json!({"people": ["John/Doe\n", "Jo/hn\n"]});
+    assert_eq!(Data::json(new_lines), data);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_array_obj_paths_and_lines() {
+    let json = json!({
+        "people": [
+            {
+                "name": "John\\Doe\r\n",
+                "nickname": "Jo\\hn\r\n",
+            }
+        ]
+    });
+    let data = Data::json(json);
+    let data = data.normalize(NormalizePaths);
+    let paths = json!({
+        "people": [
+            {
+                "name": "John/Doe\r\n",
+                "nickname": "Jo/hn\r\n",
+            }
+        ]
+    });
+    assert_eq!(Data::json(paths), data);
+    let data = data.normalize(NormalizeNewlines);
+    let new_lines = json!({
+        "people": [
+            {
+                "name": "John/Doe\n",
+                "nickname": "Jo/hn\n",
+            }
+        ]
+    });
+    assert_eq!(Data::json(new_lines), data);
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_matches_string() {
+    let exp = json!({"name": "{...}"});
+    let expected = Data::json(exp);
+    let actual = json!({"name": "JohnDoe"});
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_matches_array() {
+    let exp = json!({"people": "{...}"});
+    let expected = Data::json(exp);
+    let actual = json!({
+        "people": [
+            {
+                "name": "JohnDoe",
+                "nickname": "John",
+            }
+        ]
+    });
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_matches_obj() {
+    let exp = json!({"people": "{...}"});
+    let expected = Data::json(exp);
+    let actual = json!({
+        "people": {
+            "name": "JohnDoe",
+            "nickname": "John",
+        }
+    });
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_matches_diff_order_array() {
+    let exp = json!({
+        "people": ["John", "Jane"]
+    });
+    let expected = Data::json(exp);
+    let actual = json!({
+        "people": ["Jane", "John"]
+    });
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_ne!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_wildcard_object_first() {
+    let exp = json!({
+        "people": [
+            "{...}",
+            {
+                "name": "three",
+                "nickname": "3",
+            }
+        ]
+    });
+    let expected = Data::json(exp);
+    let actual = json!({
+        "people": [
+            {
+                "name": "one",
+                "nickname": "1",
+            },
+            {
+                "name": "two",
+                "nickname": "2",
+            },
+            {
+                "name": "three",
+                "nickname": "3",
+            }
+        ]
+    });
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_wildcard_array_first() {
+    let exp = json!([
+        "{...}",
+        {
+            "name": "three",
+            "nickname": "3",
+        }
+    ]);
+    let expected = Data::json(exp);
+    let actual = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        {
+            "name": "two",
+            "nickname": "2",
+        },
+        {
+            "name": "three",
+            "nickname": "3",
+        }
+    ]);
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_wildcard_array_first_last() {
+    let exp = json!([
+        "{...}",
+        {
+            "name": "two",
+            "nickname": "2",
+        },
+        "{...}"
+    ]);
+    let expected = Data::json(exp);
+    let actual = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        {
+            "name": "two",
+            "nickname": "2",
+        },
+        {
+            "name": "three",
+            "nickname": "3",
+        },
+        {
+            "name": "four",
+            "nickname": "4",
+        }
+    ]);
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_wildcard_array_middle_last() {
+    let exp = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        "{...}",
+        {
+            "name": "three",
+            "nickname": "3",
+        },
+        "{...}"
+    ]);
+    let expected = Data::json(exp);
+    let actual = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        {
+            "name": "two",
+            "nickname": "2",
+        },
+        {
+            "name": "three",
+            "nickname": "3",
+        },
+        {
+            "name": "four",
+            "nickname": "4",
+        },
+        {
+            "name": "five",
+            "nickname": "5",
+        }
+    ]);
+    let actual =
+        Data::json(actual).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let (DataInner::Json(exp), DataInner::Json(act)) = (expected.inner, actual.inner) {
+        assert_eq!(exp, act);
+    }
+}
+
+#[test]
+#[cfg(feature = "json")]
+fn json_normalize_wildcard_array_middle_last_early_return() {
+    let exp = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        "{...}",
+        {
+            "name": "three",
+            "nickname": "3",
+        },
+        "{...}"
+    ]);
+    let expected = Data::json(exp);
+    let actual = json!([
+        {
+            "name": "one",
+            "nickname": "1",
+        },
+        {
+            "name": "two",
+            "nickname": "2",
+        },
+        {
+            "name": "four",
+            "nickname": "4",
+        },
+        {
+            "name": "five",
+            "nickname": "5",
+        }
+    ]);
+    let actual_normalized =
+        Data::json(actual.clone()).normalize(NormalizeMatches::new(&Default::default(), &expected));
+    if let DataInner::Json(act) = actual_normalized.inner {
+        assert_eq!(act, actual);
+    }
+}