Repository: oxidecomputer/typify
Pull request #221 (2023-03-15 10:15:16)

Remove dependency on `rustfmt` installation for consumers
This PR adds two features to allow controlling pretty printing in the `TypeSpace::ToString` impl.

- `rustfmt`: use the existing behavior with `rustfmt-wrapper`
- `prettyplease`: use [prettyplease](https://docs.rs/prettyplease) to pretty print the token stream

**This is a breaking change because it changes the default behavior** (no pretty printing). This makes using this crate easier in environments where no `rustfmt` is available (e.g. https://github.com/apache/arrow-datafusion-python/issues/261).

ahl: Thanks for filing this; this is an interesting interface to consider.

In general, I like that the default for `typify` (and `progenitor`) is to use `rustfmt` for its code output that I see. I recognize, however, that 1. imposes a transitive dependency and 2. isn't important for code emitted, say, by a `build.rs`. I think, perhaps, that the right answer is for `typify` to not format code. We could remove the `impl ToString` entirely and require that consumers use `ToTokens::to_tokens()`. We could update the `build.rs` examples to do something like this:

```rust
let contents = prettyplease::unparse(parse2::<syn::File>(type_space.to_tokens()).unwrap());
```

That would probably make the most sense for the use case in the `substrait` `build.rs` file. I like this approach because it lets the consumer choose the formatting that makes the most sense for their use case.

The `cargo-typify` command (#204) can use `rustfmt` by default, but could grow some additional options (`--prettyplease` and `--unformatted`). I'd like the various test cases to render code with `rustfmt`, but that can just be a `dev-dependency`.

Please let me know what you think of this alternative direction. If you're interested in working on it, please adapt this PR. If not, please close it and I'll implement it this week.
mbrobbel: Yes I agree that's a better approach. I removed the `ToString` impl for `TypeSpace` and removed the `rustfmt-wrapper` dependency. Is that fine or should it be deprecated first?

<details>
<summary>New dependency tree</summary>

```console
cargo tree
example-build v0.0.0 (typify/example-build)
├── serde v1.0.155
│   └── serde_derive v1.0.155 (proc-macro)
│       ├── proc-macro2 v1.0.52
│       │   └── unicode-ident v1.0.8
│       ├── quote v1.0.24
│       │   └── proc-macro2 v1.0.52 (*)
│       └── syn v1.0.109
│           ├── proc-macro2 v1.0.52 (*)
│           ├── quote v1.0.24 (*)
│           └── unicode-ident v1.0.8
└── serde_json v1.0.94
    ├── itoa v1.0.1
    ├── ryu v1.0.9
    └── serde v1.0.155 (*)
[build-dependencies]
├── prettyplease v0.1.25
│   ├── proc-macro2 v1.0.52 (*)
│   └── syn v1.0.109 (*)
├── schemars v0.8.12
│   ├── dyn-clone v1.0.5
│   ├── schemars_derive v0.8.12 (proc-macro)
│   │   ├── proc-macro2 v1.0.52 (*)
│   │   ├── quote v1.0.24 (*)
│   │   ├── serde_derive_internals v0.26.0
│   │   │   ├── proc-macro2 v1.0.52 (*)
│   │   │   ├── quote v1.0.24 (*)
│   │   │   └── syn v1.0.109 (*)
│   │   └── syn v1.0.109 (*)
│   ├── serde v1.0.155 (*)
│   ├── serde_json v1.0.94 (*)
│   └── uuid v1.3.0
│       └── serde v1.0.155 (*)
├── serde_json v1.0.94 (*)
├── syn v1.0.109 (*)
└── typify v0.0.11-dev (typify/typify)
    ├── typify-impl v0.0.11-dev (typify/typify-impl)
    │   ├── heck v0.4.1
    │   ├── log v0.4.17
    │   │   └── cfg-if v1.0.0
    │   ├── proc-macro2 v1.0.52 (*)
    │   ├── quote v1.0.24 (*)
    │   ├── regress v0.5.0
    │   │   ├── hashbrown v0.13.2
    │   │   │   └── ahash v0.8.3
    │   │   │       ├── cfg-if v1.0.0
    │   │   │       └── once_cell v1.17.1
    │   │   │       [build-dependencies]
    │   │   │       └── version_check v0.9.4
    │   │   └── memchr v2.4.1
    │   ├── schemars v0.8.12 (*)
    │   ├── serde_json v1.0.94 (*)
    │   ├── syn v1.0.109 (*)
    │   ├── thiserror v1.0.39
    │   │   └── thiserror-impl v1.0.39 (proc-macro)
    │   │       ├── proc-macro2 v1.0.52 (*)
    │   │       ├── quote v1.0.24 (*)
    │   │       └── syn v1.0.109 (*)
    │   └── unicode-ident v1.0.8
    │   [dev-dependencies]
    │   ├── expectorate v1.0.6
    │   │   ├── console v0.15.3
    │   │   │   ├── lazy_static v1.4.0
    │   │   │   ├── libc v0.2.124
    │   │   │   └── unicode-width v0.1.9
    │   │   ├── newline-converter v0.2.2
    │   │   │   └── unicode-segmentation v1.10.0
    │   │   └── similar v2.2.1
    │   ├── paste v1.0.12 (proc-macro)
    │   ├── prettyplease v0.1.25 (*)
    │   ├── rustfmt-wrapper v0.2.0
    │   │   ├── serde v1.0.155 (*)
    │   │   ├── tempfile v3.3.0
    │   │   │   ├── cfg-if v1.0.0
    │   │   │   ├── fastrand v1.7.0
    │   │   │   ├── libc v0.2.124
    │   │   │   └── remove_dir_all v0.5.3
    │   │   ├── thiserror v1.0.39 (*)
    │   │   ├── toml v0.5.9
    │   │   │   └── serde v1.0.155 (*)
    │   │   └── toolchain_find v0.2.0
    │   │       ├── home v0.5.3
    │   │       ├── lazy_static v1.4.0
    │   │       ├── regex v1.6.0
    │   │       │   ├── aho-corasick v0.7.18
    │   │       │   │   └── memchr v2.4.1
    │   │       │   ├── memchr v2.4.1
    │   │       │   └── regex-syntax v0.6.27
    │   │       ├── semver v0.11.0
    │   │       │   └── semver-parser v0.10.2
    │   │       │       └── pest v2.1.3
    │   │       │           └── ucd-trie v0.1.3
    │   │       └── walkdir v2.3.2
    │   │           └── same-file v1.0.6
    │   ├── schema v0.0.1
    │   │   ├── proc-macro2 v1.0.52 (*)
    │   │   ├── quote v1.0.24 (*)
    │   │   ├── schema-derive v0.0.1 (proc-macro)
    │   │   │   ├── paste v1.0.12 (proc-macro)
    │   │   │   ├── proc-macro2 v1.0.52 (*)
    │   │   │   ├── quote v1.0.24 (*)
    │   │   │   └── syn v1.0.109 (*)
    │   │   └── syn v1.0.109 (*)
    │   ├── schemars v0.8.12 (*)
    │   ├── serde v1.0.155 (*)
    │   └── uuid v1.3.0 (*)
    └── typify-macro v0.0.11-dev (proc-macro) (typify/typify-macro)
        ├── proc-macro2 v1.0.52 (*)
        ├── quote v1.0.24 (*)
        ├── schemars v0.8.12 (*)
        ├── serde v1.0.155 (*)
        ├── serde_json v1.0.94 (*)
        ├── serde_tokenstream v0.1.7
        │   ├── proc-macro2 v1.0.52 (*)
        │   ├── serde v1.0.155 (*)
        │   └── syn v1.0.109 (*)
        ├── syn v1.0.109 (*)
        └── typify-impl v0.0.11-dev (typify/typify-impl) (*)
    [dev-dependencies]
    ├── expectorate v1.0.6 (*)
    ├── glob v0.3.1
    ├── quote v1.0.24 (*)
    ├── regress v0.5.0 (*)
    ├── rustfmt-wrapper v0.2.0 (*)
    ├── schemars v0.8.12 (*)
    ├── serde v1.0.155 (*)
    ├── serde_json v1.0.94 (*)
    ├── trybuild v1.0.79
    │   ├── basic-toml v0.1.1
    │   │   └── serde v1.0.155 (*)
    │   ├── glob v0.3.1
    │   ├── once_cell v1.17.1
    │   ├── serde v1.0.155 (*)
    │   ├── serde_derive v1.0.155 (proc-macro) (*)
    │   ├── serde_json v1.0.94 (*)
    │   └── termcolor v1.1.3
    └── uuid v1.3.0 (*)

example-macro v0.0.0 (typify/example-macro)
├── serde v1.0.155 (*)
├── serde_json v1.0.94 (*)
└── typify v0.0.11-dev (typify/typify) (*)

typify v0.0.11-dev (typify/typify) (*)

typify-impl v0.0.11-dev (typify/typify-impl) (*)

typify-macro v0.0.11-dev (proc-macro) (typify/typify-macro) (*)

typify-test v0.0.0 (typify/typify-test)
├── regress v0.5.0 (*)
├── serde v1.0.155 (*)
└── serde_json v1.0.94 (*)
[build-dependencies]
├── ipnetwork v0.20.0
│   ├── schemars v0.8.12 (*)
│   └── serde v1.0.155 (*)
├── prettyplease v0.1.25 (*)
├── schemars v0.8.12 (*)
├── serde v1.0.155 (*)
├── syn v1.0.109 (*)
└── typify v0.0.11-dev (typify/typify) (*)
```

</details>
jayvdb: Fwiw, I use `prettyplease` followed by `rustfmt` to clean up the generated code.  `rustfmt` does a really bad job for formatting generated code - it ignores lots of it - there are some tips to see how bad at https://github.com/rust-lang/rustfmt/issues/5683 .
After `prettyplease` has reformatted the generated code, `rustfmt` does a much better job.

Patch:
diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 6555ae2..d94c0eb 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -13,6 +13,25 @@
 
 == Unreleased changes (release date TBD)
 
+Breaking change: this release removes the `ToString` implementation for
+`TypeSpace` and the `rustfmt-wrapper` dependency. This allows users to use a
+pretty printing method of choice. Users can use the `ToTokens` implementation of
+`TypeSpace`:
+
+Before:
+[source,rust]
+----
+let typespace = TypeSpace::default();
+typespace.to_string()
+----
+
+After:
+[source,rust]
+----
+let typespace = TypeSpace::default();
+typespace.to_stream().to_string()
+----
+
 https://github.com/oxidecomputer/typify/compare/v0.0.10\...HEAD[Full list of commits]
 
 * Allow per-type renames and derive macro applications (#131)
diff --git a/Cargo.lock b/Cargo.lock
index 6448d64..d5884f4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -253,9 +253,11 @@ dependencies = [
 name = "example-build"
 version = "0.0.0"
 dependencies = [
+ "prettyplease",
  "schemars",
  "serde",
  "serde_json",
+ "syn",
  "typify",
 ]
 
@@ -537,6 +539,16 @@ dependencies = [
  "termtree",
 ]
 
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
 [[package]]
 name = "proc-macro-error"
 version = "1.0.4"
@@ -1074,10 +1086,12 @@ name = "typify-test"
 version = "0.0.0"
 dependencies = [
  "ipnetwork",
+ "prettyplease",
  "regress",
  "schemars",
  "serde",
  "serde_json",
+ "syn",
  "typify",
 ]
 
diff --git a/README.md b/README.md
index bc33051..4df6bb8 100644
--- a/README.md
+++ b/README.md
@@ -3,17 +3,18 @@
 Typify compiles JSON Schema documents into Rust types. It can be used in one of
 three ways:
 
-- via the macro `import_types!("types.json")` to generate Rust types directly
-in your program
+- via the macro `import_types!("types.json")` to generate Rust types directly in
+  your program
 
 - via a builder interface to generate Rust types in `build.rs`
 
 - or via the builder functions to generate persistent files e.g. when building
-API bindings.
+  API bindings.
 
 ## JSON Schema → Rust types
 
-Typify translates JSON Schema types in a few different ways depending on some basic properties of the schema:
+Typify translates JSON Schema types in a few different ways depending on some
+basic properties of the schema:
 
 ### Built-in types
 
@@ -23,17 +24,17 @@ appropriate built-in type based on type attributes. For example, a JSON Schema
 might specify a maximum and/or minimum that indicates the appropriate integral
 type to use.
 
-String schemas that include a `format` are represented with the appropriate
-Rust type. For example `{ "type": "string", "format": "uuid" }` is represented
-as a `uuid::Uuid` (which requires the `uuid` crate be included as a dependency).
+String schemas that include a `format` are represented with the appropriate Rust
+type. For example `{ "type": "string", "format": "uuid" }` is represented as a
+`uuid::Uuid` (which requires the `uuid` crate be included as a dependency).
 
 ### Arrays
 
-JSON Schema arrays can turn into one of three Rust types `Vec<T>`,
-`HashSet<T>`, and tuples depending on the schema properties. An array may have
-a fixed length that matches a fixed list of item types; this is well
-represented by a Rust tuples. The distinction between `Vec<T>` and `HashSet<T>`
-is only if the schema's `uniqueItems` field is `false` or `true` respectively.
+JSON Schema arrays can turn into one of three Rust types `Vec<T>`, `HashSet<T>`,
+and tuples depending on the schema properties. An array may have a fixed length
+that matches a fixed list of item types; this is well represented by a Rust
+tuples. The distinction between `Vec<T>` and `HashSet<T>` is only if the
+schema's `uniqueItems` field is `false` or `true` respectively.
 
 ### Objects
 
@@ -49,7 +50,8 @@ simply get the `#[serde(default)]` attribute (so you won't see e.g.
 
 ### OneOf
 
-The `OneOf` construct maps to a Rust enum. Typify maps this to the various [serde enum types](https://serde.rs/enum-representations.html).
+The `OneOf` construct maps to a Rust enum. Typify maps this to the various
+[serde enum types](https://serde.rs/enum-representations.html).
 
 ### AnyOf / AllOf
 
@@ -57,6 +59,34 @@ The `anyOf` and `allOf` constructs are a little trickier to handle, but (in
 general) Typify models these as structs where each member is decorated with the
 `#[serde(flatten)]` attribute (with `Option` wrappers in the case of `anyOf`).
 
+## Formatting
+
+By default Typify's generated code is not formatted. If formatted code is
+preferred, crates like [rustfmt-wrapper](https://docs.rs/rustfmt-wrapper) and
+[prettyplease](https://docs.rs/prettyplease) can be used to format the generated
+code before writing it to a file.
+
+The examples below show different ways to convert a `TypeSpace` to a string
+(`typespace` is a `typify::TypeSpace`).
+
+### No formatting
+
+```rust
+typespace.to_stream().to_string()
+```
+
+### Rustfmt
+
+```rust
+rustfmt_wrapper::rustfmt(typespace.to_stream().to_string())?
+```
+
+### Prettyplease
+
+```rust
+prettyplease::unparse(&syn::parse2::<syn::File>(typespace.to_stream())?)
+```
+
 ## WIP
 
 Typify is a work in progress. Changes that affect output will be indicated with
@@ -73,9 +103,9 @@ Bounded numbers aren't very well handled. Consider, for example, the schema:
 
 ```json
 {
-    "type": "integer",
-    "minimum": 1,
-    "maximum": 6
+  "type": "integer",
+  "minimum": 1,
+  "maximum": 6
 }
 ```
 
@@ -98,4 +128,5 @@ struct A {
     a: Box<A>,
 }
 ```
+
 .. but it does not support more complex cycles such as A -> B -> A.
diff --git a/cargo-typify/src/lib.rs b/cargo-typify/src/lib.rs
index d13517f..fcfbd86 100644
--- a/cargo-typify/src/lib.rs
+++ b/cargo-typify/src/lib.rs
@@ -99,7 +99,7 @@ pub fn convert(args: &CliArgs) -> Result<String> {
 use serde::{Deserialize, Serialize};
 ";
 
-    let contents = format!("{intro}\n{}", type_space.to_string());
+    let contents = format!("{intro}\n{}", type_space.to_stream().to_string());
 
     let contents = rustfmt_wrapper::rustfmt(contents).wrap_err("Failed to format Rust code")?;
 
diff --git a/example-build/Cargo.toml b/example-build/Cargo.toml
index 751c54b..72985f4 100644
--- a/example-build/Cargo.toml
+++ b/example-build/Cargo.toml
@@ -9,6 +9,8 @@ serde = "1.0"
 serde_json = "1.0.94"
 
 [build-dependencies]
+prettyplease = "0.1"
 schemars = "0.8"
 serde_json = "1.0.94"
+syn = "1.0.109"
 typify = { path = "../typify" }
diff --git a/example-build/build.rs b/example-build/build.rs
index c24415b..71af1b9 100644
--- a/example-build/build.rs
+++ b/example-build/build.rs
@@ -18,7 +18,7 @@ fn main() {
     let contents = format!(
         "{}\n{}",
         "use serde::{Deserialize, Serialize};",
-        type_space.to_string()
+        prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap())
     );
 
     let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();
diff --git a/typify-impl/Cargo.toml b/typify-impl/Cargo.toml
index 6faa81f..3be0134 100644
--- a/typify-impl/Cargo.toml
+++ b/typify-impl/Cargo.toml
@@ -14,7 +14,6 @@ log = "0.4"
 proc-macro2 = "1.0"
 quote = "1.0"
 regress = "0.5.0"
-rustfmt-wrapper = "0.2"
 schemars = "0.8.12"
 serde_json = "1.0"
 syn = { version = "1.0", features = ["full"] }
@@ -24,6 +23,7 @@ unicode-ident = "1.0.8"
 [dev-dependencies]
 expectorate = "1.0"
 paste = "1.0"
+rustfmt-wrapper = "0.2"
 schema = "0.0.1"
 schemars = { version = "0.8.12", features = ["uuid1"] }
 serde = "1.0"
diff --git a/typify-impl/src/convert.rs b/typify-impl/src/convert.rs
index 50580e1..32bbbbc 100644
--- a/typify-impl/src/convert.rs
+++ b/typify-impl/src/convert.rs
@@ -861,9 +861,11 @@ impl TypeSpace {
     fn convert_number<'a>(
         &self,
         _metadata: &'a Option<Box<Metadata>>,
-        validation: &Option<Box<schemars::schema::NumberValidation>>,
+        _validation: &Option<Box<schemars::schema::NumberValidation>>,
         _format: &Option<String>,
     ) -> Result<(TypeEntry, &'a Option<Box<Metadata>>)> {
+        /*
+        See https://github.com/oxidecomputer/typify/issues/169
         if let Some(validation) = validation {
             assert!(validation.multiple_of.is_none());
             assert!(validation.maximum.is_none());
@@ -871,6 +873,7 @@ impl TypeSpace {
             assert!(validation.minimum.is_none());
             assert!(validation.exclusive_minimum.is_none());
         }
+        */
 
         Ok((TypeEntry::new_float("f64"), &None))
     }
diff --git a/typify-impl/src/lib.rs b/typify-impl/src/lib.rs
index 5664f64..25329df 100644
--- a/typify-impl/src/lib.rs
+++ b/typify-impl/src/lib.rs
@@ -7,7 +7,6 @@ use log::info;
 use output::OutputSpace;
 use proc_macro2::TokenStream;
 use quote::{quote, ToTokens};
-use rustfmt_wrapper::rustfmt;
 use schemars::schema::{Metadata, Schema};
 use thiserror::Error;
 use type_entry::{
@@ -801,12 +800,6 @@ impl TypeSpace {
     }
 }
 
-impl ToString for TypeSpace {
-    fn to_string(&self) -> String {
-        rustfmt(self.to_stream().to_string()).unwrap()
-    }
-}
-
 impl ToTokens for TypeSpace {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         tokens.extend(self.to_stream())


Test patch:
diff --git a/cargo-typify/tests/integration.rs b/cargo-typify/tests/integration.rs
index 17fd522..7b31319 100644
--- a/cargo-typify/tests/integration.rs
+++ b/cargo-typify/tests/integration.rs
@@ -1,5 +1,5 @@
 use expectorate::assert_contents;
-use newline_converter::{dos2unix, unix2dos};
+use newline_converter::dos2unix;
 use tempdir::TempDir;
 
 #[test]
@@ -19,12 +19,9 @@ fn test_simple() {
         .assert()
         .success();
 
-    let content = std::fs::read_to_string(output_file).unwrap();
+    let actual = std::fs::read_to_string(output_file).unwrap();
 
-    assert_contents(
-        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/outputs/builder.rs"),
-        &content,
-    );
+    assert_contents("tests/outputs/builder.rs", &actual);
 }
 
 #[test]
@@ -43,10 +40,7 @@ fn test_default_output() {
 
     let content = std::fs::read_to_string(output_file).unwrap();
 
-    assert_contents(
-        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/outputs/builder.rs"),
-        &content,
-    );
+    assert_contents("tests/outputs/builder.rs", &content);
 }
 
 #[test]
@@ -57,14 +51,6 @@ fn test_positional_stdout() {
 
     let mut cmd = Command::cargo_bin("cargo-typify").unwrap();
 
-    let expected = std::fs::read_to_string(concat!(
-        env!("CARGO_MANIFEST_DIR"),
-        "/tests/outputs/positional.rs"
-    ))
-    .unwrap();
-
-    let expected = dos2unix(&expected);
-
     let output = cmd
         .args(["typify", input, "--positional", "--output", "-"])
         .output()
@@ -74,7 +60,7 @@ fn test_positional_stdout() {
     let actual = dos2unix(&output_stdout);
 
     assert!(output.status.success());
-    assert_eq!(actual, expected);
+    assert_contents("tests/outputs/positional.rs", &actual);
 }
 
 #[test]
@@ -97,12 +83,9 @@ fn test_builder() {
     .assert()
     .success();
 
-    let content = std::fs::read_to_string(output_file).unwrap();
+    let actual = std::fs::read_to_string(output_file).unwrap();
 
-    assert_contents(
-        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/outputs/builder.rs"),
-        &content,
-    );
+    assert_contents("tests/outputs/builder.rs", &actual);
 }
 
 #[test]
@@ -127,12 +110,9 @@ fn test_derive() {
     .assert()
     .success();
 
-    let content = std::fs::read_to_string(output_file).unwrap();
+    let actual = std::fs::read_to_string(output_file).unwrap();
 
-    assert_contents(
-        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/outputs/derive.rs"),
-        &content,
-    );
+    assert_contents("tests/outputs/derive.rs", &actual);
 }
 
 #[test]
@@ -159,12 +139,9 @@ fn test_multi_derive() {
     .assert()
     .success();
 
-    let content = std::fs::read_to_string(output_file).unwrap();
+    let actual = std::fs::read_to_string(output_file).unwrap();
 
-    assert_contents(
-        concat!(env!("CARGO_MANIFEST_DIR"), "/tests/outputs/multi_derive.rs"),
-        &content,
-    );
+    assert_contents("tests/outputs/multi_derive.rs", &actual);
 }
 
 #[test]
@@ -173,19 +150,11 @@ fn test_help() {
 
     let mut cmd = Command::cargo_bin("cargo-typify").unwrap();
 
-    let expected = std::fs::read_to_string(concat!(
-        env!("CARGO_MANIFEST_DIR"),
-        "/tests/outputs/help.txt"
-    ))
-    .unwrap();
-
-    let expected = dos2unix(&expected);
-
     let output = cmd.args(["typify", "--help"]).output().unwrap();
 
     let output_stdout = String::from_utf8(output.stdout).unwrap();
     let actual = dos2unix(&output_stdout);
 
     assert!(output.status.success());
-    assert_eq!(actual, expected);
+    assert_contents("tests/outputs/help.txt", &actual);
 }
diff --git a/typify-test/Cargo.toml b/typify-test/Cargo.toml
index fda2b3c..399e9f7 100644
--- a/typify-test/Cargo.toml
+++ b/typify-test/Cargo.toml
@@ -11,6 +11,8 @@ serde_json = "1.0.94"
 
 [build-dependencies]
 ipnetwork = { version = "0.20.0", features = ["schemars"] }
+prettyplease = "0.1"
 schemars = "0.8.12"
 serde = "1.0.155"
+syn = "1.0.109"
 typify = { path = "../typify" }
diff --git a/typify-test/build.rs b/typify-test/build.rs
index 20a34e8..1933770 100644
--- a/typify-test/build.rs
+++ b/typify-test/build.rs
@@ -109,7 +109,7 @@ fn main() {
     let contents = format!(
         "{}\n{}",
         "use serde::{Deserialize, Serialize};",
-        type_space.to_string()
+        prettyplease::unparse(&syn::parse2::<syn::File>(type_space.to_stream()).unwrap())
     );
 
     let mut out_file = Path::new(&env::var("OUT_DIR").unwrap()).to_path_buf();