Repository: achanda/ipnetwork (language: rust)

Issue:
JSON schema for IpNetwork does not match its serialized form
See: https://github.com/ahl/ipnetwork/tree/fail

```console
$ cargo test --features=schemars test_ipnetwork_json
    Blocking waiting for file lock on build directory
   Compiling does-it-json v0.0.1 (/Users/ahl/src/does-it-json)
   Compiling ipnetwork v0.19.0 (/Users/ahl/src/ipnetwork)
    Finished test [unoptimized + debuginfo] target(s) in 2.20s
     Running unittests src/lib.rs (target/debug/deps/ipnetwork-5bc3e27d373077a5)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 63 filtered out; finished in 0.00s

     Running tests/test_json.rs (target/debug/deps/test_json-c7e0909f58ffe5cf)

running 1 test
test tests::test_ipnetwork_json ... FAILED

failures:

---- tests::test_ipnetwork_json stdout ----
thread 'tests::test_ipnetwork_json' panicked at 'error: "127.1.0.0/24" did not conform to the schema at #/definitions/IpNetwork.oneOf: value validated against 0 of 2 `oneOf` schemas (rather than 1)
schema: {
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "MyStruct",
  "type": "object",
  "required": [
    "ipnetwork"
  ],
  "properties": {
    "ipnetwork": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/IpNetwork"
      }
    }
  },
  "definitions": {
    "IpNetwork": {
      "description": "Represents a generic network range. This type can have two variants: the v4 and the v6 case.",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "V4"
          ],
          "properties": {
            "V4": {
              "$ref": "#/definitions/Ipv4Network"
            }
          },
          "additionalProperties": false
        },
        {
          "type": "object",
          "required": [
            "V6"
          ],
          "properties": {
            "V6": {
              "$ref": "#/definitions/Ipv6Network"
            }
          },
          "additionalProperties": false
        }
      ]
    },
    "Ipv4Network": {
      "description": "Represents a network range where the IP addresses are of v4",
      "type": "object",
      "required": [
        "addr",
        "prefix"
      ],
      "properties": {
        "addr": {
          "type": "string",
          "format": "ipv4"
        },
        "prefix": {
          "type": "integer",
          "format": "uint8",
          "minimum": 0.0
        }
      }
    },
    "Ipv6Network": {
      "description": "Represents a network range where the IP addresses are of v6",
      "type": "object",
      "required": [
        "addr",
        "prefix"
      ],
      "properties": {
        "addr": {
          "type": "string",
          "format": "ipv6"
        },
        "prefix": {
          "type": "integer",
          "format": "uint8",
          "minimum": 0.0
        }
      }
    }
  }
}
value: {
  "ipnetwork": [
    "127.1.0.0/24",
    "::1/0"
  ]
}', tests/test_json.rs:69:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::test_ipnetwork_json

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s

error: test failed, to rerun pass '--test test_json'
```

The problem is that #153 simply derived `JsonSchema` rather than generating a schema that matches the implementation of `Serialize`. I would be happy to submit a PR; just let me know.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index 0c35f5a..adbde45 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -23,6 +23,6 @@ jobs:
     - name: Build
       run: cargo build --verbose
     - name: Run tests
-      run: cargo test --verbose
+      run: cargo test --verbose --all-features
     - name: Build docs
       run: cargo doc --verbose
diff --git a/Cargo.toml b/Cargo.toml
index 5f7e589..fe702f5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,8 +16,8 @@ schemars = { version = "0.8.10", optional = true }
 
 [dev-dependencies]
 serde_json = "1.0"
-serde_derive = "1"
 criterion = {version = "0.3.4", features= ["html_reports"]}
+does-it-json = "0.0.3"
 
 [badges]
 travis-ci = { repository = "achanda/ipnetwork" }
diff --git a/src/ipv4.rs b/src/ipv4.rs
index c681b2c..d162fb6 100644
--- a/src/ipv4.rs
+++ b/src/ipv4.rs
@@ -1,11 +1,10 @@
 use crate::common::{cidr_parts, parse_prefix, IpNetworkError};
-use std::{fmt, net::Ipv4Addr, str::FromStr, convert::TryFrom};
+use std::{convert::TryFrom, fmt, net::Ipv4Addr, str::FromStr};
 
 const IPV4_BITS: u8 = 32;
 
 /// Represents a network range where the IP addresses are of v4
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct Ipv4Network {
     addr: Ipv4Addr,
     prefix: u8,
@@ -32,6 +31,36 @@ impl serde::Serialize for Ipv4Network {
     }
 }
 
+#[cfg(feature = "schemars")]
+impl schemars::JsonSchema for Ipv4Network {
+    fn schema_name() -> String {
+        "Ipv4Network".to_string()
+    }
+
+    fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        schemars::schema::SchemaObject {
+            instance_type: Some(schemars::schema::InstanceType::String.into()),
+            string: Some(Box::new(schemars::schema::StringValidation {
+                pattern: Some(
+                    concat!(
+                        r#"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"#,
+                        r#"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"#,
+                        r#"\/(3[0-2]|[0-2]?[0-9])$"#,
+                    )
+                    .to_string(),
+                ),
+                ..Default::default()
+            })),
+            extensions: [("x-rust-type".to_string(), "ipnetwork::Ipv4Network".into())]
+                .iter()
+                .cloned()
+                .collect(),
+            ..Default::default()
+        }
+        .into()
+    }
+}
+
 impl Ipv4Network {
     /// Constructs a new `Ipv4Network` from any `Ipv4Addr` and a prefix denoting the network size.
     ///
diff --git a/src/ipv6.rs b/src/ipv6.rs
index dfce6c4..b2299df 100644
--- a/src/ipv6.rs
+++ b/src/ipv6.rs
@@ -1,12 +1,11 @@
 use crate::common::{cidr_parts, parse_prefix, IpNetworkError};
-use std::{cmp, fmt, net::Ipv6Addr, str::FromStr, convert::TryFrom};
+use std::{cmp, convert::TryFrom, fmt, net::Ipv6Addr, str::FromStr};
 
 const IPV6_BITS: u8 = 128;
 const IPV6_SEGMENT_BITS: u8 = 16;
 
 /// Represents a network range where the IP addresses are of v6
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub struct Ipv6Network {
     addr: Ipv6Addr,
     prefix: u8,
@@ -33,6 +32,46 @@ impl serde::Serialize for Ipv6Network {
     }
 }
 
+#[cfg(feature = "schemars")]
+impl schemars::JsonSchema for Ipv6Network {
+    fn schema_name() -> String {
+        "Ipv6Network".to_string()
+    }
+
+    fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        schemars::schema::SchemaObject {
+            instance_type: Some(schemars::schema::InstanceType::String.into()),
+            string: Some(Box::new(schemars::schema::StringValidation {
+                pattern: Some(
+                    concat!(
+                        r#"^("#,
+                        r#"([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,7}:"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}"#,
+                        r#"|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})"#,
+                        r#"|:((:[0-9a-fA-F]{1,4}){1,7}|:)"#,
+                        r#"|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}"#,
+                        r#"|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"#,
+                        r#"|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])"#,
+                        r#"")[/](12[0-8]|1[0-1][0-9]|[0-9]?[0-9])$"#,
+                    ).to_string(),
+                ),
+                ..Default::default()
+            })),
+            extensions: [("x-rust-type".to_string(), "ipnetwork::Ipv6Network".into())]
+                .iter()
+                .cloned()
+                .collect(),
+            ..Default::default()
+        }
+        .into()
+    }
+}
+
 impl Ipv6Network {
     /// Constructs a new `Ipv6Network` from any `Ipv6Addr` and a prefix denoting the network size.
     ///
diff --git a/src/lib.rs b/src/lib.rs
index 546e902..8d3e353 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,14 +1,14 @@
 //! The `ipnetwork` crate provides a set of APIs to work with IP CIDRs in
 //! Rust.
 #![crate_type = "lib"]
-
 #![deny(
     missing_debug_implementations,
     unsafe_code,
     unused_extern_crates,
-    unused_import_braces)]
+    unused_import_braces
+)]
 
-use std::{fmt, net::IpAddr, str::FromStr, convert::TryFrom};
+use std::{convert::TryFrom, fmt, net::IpAddr, str::FromStr};
 
 mod common;
 mod ipv4;
@@ -23,7 +23,6 @@ pub use crate::ipv6::{ipv6_mask_to_prefix, Ipv6Network};
 /// Represents a generic network range. This type can have two variants:
 /// the v4 and the v6 case.
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
-#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
 pub enum IpNetwork {
     V4(Ipv4Network),
     V6(Ipv6Network),
@@ -50,6 +49,74 @@ impl serde::Serialize for IpNetwork {
     }
 }
 
+#[cfg(feature = "schemars")]
+impl schemars::JsonSchema for IpNetwork {
+    fn schema_name() -> String {
+        "IpNetwork".to_string()
+    }
+
+    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
+        schemars::schema::SchemaObject {
+            metadata: Some(
+                schemars::schema::Metadata {
+                    ..Default::default()
+                }
+                .into(),
+            ),
+            subschemas: Some(
+                schemars::schema::SubschemaValidation {
+                    one_of: Some(vec![
+                        schemars::schema::SchemaObject {
+                            metadata: Some(
+                                schemars::schema::Metadata {
+                                    title: Some("v4".to_string()),
+                                    ..Default::default()
+                                }
+                                .into(),
+                            ),
+                            subschemas: Some(
+                                schemars::schema::SubschemaValidation {
+                                    all_of: Some(vec![gen.subschema_for::<Ipv4Network>()]),
+                                    ..Default::default()
+                                }
+                                .into(),
+                            ),
+                            ..Default::default()
+                        }
+                        .into(),
+                        schemars::schema::SchemaObject {
+                            metadata: Some(
+                                schemars::schema::Metadata {
+                                    title: Some("v6".to_string()),
+                                    ..Default::default()
+                                }
+                                .into(),
+                            ),
+                            subschemas: Some(
+                                schemars::schema::SubschemaValidation {
+                                    all_of: Some(vec![gen.subschema_for::<Ipv6Network>()]),
+                                    ..Default::default()
+                                }
+                                .into(),
+                            ),
+                            ..Default::default()
+                        }
+                        .into(),
+                    ]),
+                    ..Default::default()
+                }
+                .into(),
+            ),
+            extensions: [("x-rust-type".to_string(), "ipnetwork::IpNetwork".into())]
+                .iter()
+                .cloned()
+                .collect(),
+            ..Default::default()
+        }
+        .into()
+    }
+}
+
 /// Represents a generic network size. For IPv4, the max size is a u32 and for IPv6, it is a u128
 #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
 pub enum NetworkSize {


Test patch:
diff --git a/tests/test_json.rs b/tests/test_json.rs
index 2ad039f..77f8709 100644
--- a/tests/test_json.rs
+++ b/tests/test_json.rs
@@ -3,7 +3,7 @@
 #[cfg(test)]
 mod tests {
     use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network};
-    use serde_derive::{Deserialize, Serialize};
+    use serde::{Deserialize, Serialize};
     use std::net::{Ipv4Addr, Ipv6Addr};
 
     #[test]
@@ -11,6 +11,7 @@ mod tests {
         let json_string = r#"{"ipnetwork":"127.1.0.0/24"}"#;
 
         #[derive(Serialize, Deserialize)]
+        #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
         struct MyStruct {
             ipnetwork: Ipv4Network,
         }
@@ -21,6 +22,11 @@ mod tests {
         assert_eq!(mystruct.ipnetwork.prefix(), 24);
 
         assert_eq!(::serde_json::to_string(&mystruct).unwrap(), json_string);
+
+        #[cfg(feature = "schemars")]
+        if let Err(s) = does_it_json::validate_with_output(&mystruct) {
+            panic!("{}", s);
+        }
     }
 
     #[test]
@@ -28,6 +34,7 @@ mod tests {
         let json_string = r#"{"ipnetwork":"::1/0"}"#;
 
         #[derive(Serialize, Deserialize)]
+        #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
         struct MyStruct {
             ipnetwork: Ipv6Network,
         }
@@ -41,6 +48,11 @@ mod tests {
         assert_eq!(mystruct.ipnetwork.prefix(), 0);
 
         assert_eq!(::serde_json::to_string(&mystruct).unwrap(), json_string);
+
+        #[cfg(feature = "schemars")]
+        if let Err(s) = does_it_json::validate_with_output(&mystruct) {
+            panic!("{}", s);
+        }
     }
 
     #[test]
@@ -48,6 +60,7 @@ mod tests {
         let json_string = r#"{"ipnetwork":["127.1.0.0/24","::1/0"]}"#;
 
         #[derive(Serialize, Deserialize)]
+        #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
         struct MyStruct {
             ipnetwork: Vec<IpNetwork>,
         }
@@ -63,5 +76,10 @@ mod tests {
         assert_eq!(mystruct.ipnetwork[1].prefix(), 0);
 
         assert_eq!(::serde_json::to_string(&mystruct).unwrap(), json_string);
+
+        #[cfg(feature = "schemars")]
+        if let Err(s) = does_it_json::validate_with_output(&mystruct) {
+            panic!("{}", s);
+        }
     }
 }