Repository: CosmWasm/cosmwasm (rust)

Issue:
schema_for! on HashMap and BTreeMap no longer generates field information for values
Sorry for the confusing title, allow me to demonstrate. I bisected this issue down to the commit:

https://github.com/CosmWasm/cosmwasm/commit/e4700750e12fe7f0e5b4ab157e4a085f192358b1

You can see the issue with this code:

```rust
use cosmwasm_schema::cw_serde;
use std::collections::BTreeMap;

#[cw_serde]
struct HasMap {
    map_field: BTreeMap<String, u32>,
    normal_field: u32,
}

fn main() {
    let schema = cosmwasm_schema::schema_for!(HasMap);
    println!("{schema:#?}");
}
```

With cosmwasm-schema version 2.0.2, the output I receive is:

<details>

```json
RootSchema {
    meta_schema: Some(
        "http://json-schema.org/draft-07/schema#",
    ),
    schema: SchemaObject {
        metadata: Some(
            Metadata {
                id: None,
                title: Some(
                    "HasMap",
                ),
                description: None,
                default: None,
                deprecated: false,
                read_only: false,
                write_only: false,
                examples: [],
            },
        ),
        instance_type: Some(
            Single(
                Object,
            ),
        ),
        format: None,
        enum_values: None,
        const_value: None,
        subschemas: None,
        number: None,
        string: None,
        array: None,
        object: Some(
            ObjectValidation {
                max_properties: None,
                min_properties: None,
                required: {
                    "map_field",
                    "normal_field",
                },
                properties: {
                    "map_field": Object(
                        SchemaObject {
                            metadata: None,
                            instance_type: Some(
                                Single(
                                    Object,
                                ),
                            ),
                            format: None,
                            enum_values: None,
                            const_value: None,
                            subschemas: None,
                            number: None,
                            string: None,
                            array: None,
                            object: Some(
                                ObjectValidation {
                                    max_properties: None,
                                    min_properties: None,
                                    required: {},
                                    properties: {},
                                    pattern_properties: {},
                                    additional_properties: Some(
                                        Object(
                                            SchemaObject {
                                                metadata: None,
                                                instance_type: Some(
                                                    Single(
                                                        Integer,
                                                    ),
                                                ),
                                                format: Some(
                                                    "uint32",
                                                ),
                                                enum_values: None,
                                                const_value: None,
                                                subschemas: None,
                                                number: Some(
                                                    NumberValidation {
                                                        multiple_of: None,
                                                        maximum: None,
                                                        exclusive_maximum: None,
                                                        minimum: Some(
                                                            0.0,
                                                        ),
                                                        exclusive_minimum: None,
                                                    },
                                                ),
                                                string: None,
                                                array: None,
                                                object: None,
                                                reference: None,
                                                extensions: {},
                                            },
                                        ),
                                    ),
                                    property_names: None,
                                },
                            ),
                            reference: None,
                            extensions: {},
                        },
                    ),
                    "normal_field": Object(
                        SchemaObject {
                            metadata: None,
                            instance_type: Some(
                                Single(
                                    Integer,
                                ),
                            ),
                            format: Some(
                                "uint32",
                            ),
                            enum_values: None,
                            const_value: None,
                            subschemas: None,
                            number: Some(
                                NumberValidation {
                                    multiple_of: None,
                                    maximum: None,
                                    exclusive_maximum: None,
                                    minimum: Some(
                                        0.0,
                                    ),
                                    exclusive_minimum: None,
                                },
                            ),
                            string: None,
                            array: None,
                            object: None,
                            reference: None,
                            extensions: {},
                        },
                    ),
                },
                pattern_properties: {},
                additional_properties: Some(
                    Bool(
                        false,
                    ),
                ),
                property_names: None,
            },
        ),
        reference: None,
        extensions: {},
    },
    definitions: {},
}
```

</details>

But with version 2.1.2, instead I get:

<details>

```json
RootSchema {
    meta_schema: Some(
        "http://json-schema.org/draft-07/schema#",
    ),
    schema: SchemaObject {
        metadata: Some(
            Metadata {
                id: None,
                title: Some(
                    "HasMap",
                ),
                description: None,
                default: None,
                deprecated: false,
                read_only: false,
                write_only: false,
                examples: [],
            },
        ),
        instance_type: Some(
            Single(
                Object,
            ),
        ),
        format: None,
        enum_values: None,
        const_value: None,
        subschemas: None,
        number: None,
        string: None,
        array: None,
        object: Some(
            ObjectValidation {
                max_properties: None,
                min_properties: None,
                required: {
                    "map_field",
                    "normal_field",
                },
                properties: {
                    "map_field": Object(
                        SchemaObject {
                            metadata: None,
                            instance_type: Some(
                                Single(
                                    Object,
                                ),
                            ),
                            format: None,
                            enum_values: None,
                            const_value: None,
                            subschemas: None,
                            number: None,
                            string: None,
                            array: None,
                            object: Some(
                                ObjectValidation {
                                    max_properties: None,
                                    min_properties: None,
                                    required: {},
                                    properties: {},
                                    pattern_properties: {},
                                    additional_properties: Some(
                                        Bool(
                                            false,
                                        ),
                                    ),
                                    property_names: None,
                                },
                            ),
                            reference: None,
                            extensions: {},
                        },
                    ),
                    "normal_field": Object(
                        SchemaObject {
                            metadata: None,
                            instance_type: Some(
                                Single(
                                    Integer,
                                ),
                            ),
                            format: Some(
                                "uint32",
                            ),
                            enum_values: None,
                            const_value: None,
                            subschemas: None,
                            number: Some(
                                NumberValidation {
                                    multiple_of: None,
                                    maximum: None,
                                    exclusive_maximum: None,
                                    minimum: Some(
                                        0.0,
                                    ),
                                    exclusive_minimum: None,
                                },
                            ),
                            string: None,
                            array: None,
                            object: None,
                            reference: None,
                            extensions: {},
                        },
                    ),
                },
                pattern_properties: {},
                additional_properties: Some(
                    Bool(
                        false,
                    ),
                ),
                property_names: None,
            },
        ),
        reference: None,
        extensions: {},
    },
    definitions: {},
}
```

</details>

Since that's a lot of content, here's the diff between the two:

```diff
<                                         Object(
<                                             SchemaObject {
<                                                 metadata: None,
<                                                 instance_type: Some(
<                                                     Single(
<                                                         Integer,
<                                                     ),
<                                                 ),
<                                                 format: Some(
<                                                     "uint32",
<                                                 ),
<                                                 enum_values: None,
<                                                 const_value: None,
<                                                 subschemas: None,
<                                                 number: Some(
<                                                     NumberValidation {
<                                                         multiple_of: None,
<                                                         maximum: None,
<                                                         exclusive_maximum: None,
<                                                         minimum: Some(
<                                                             0.0,
<                                                         ),
<                                                         exclusive_minimum: None,
<                                                     },
<                                                 ),
<                                                 string: None,
<                                                 array: None,
<                                                 object: None,
<                                                 reference: None,
<                                                 extensions: {},
<                                             },
---
>                                         Bool(
>                                             false,
```

Note that the old version correctly generated schema information indicating that the contents of the field are an Object with `uint32`s as values. Without this, the generated schemas are missing that information, which ultimately (in our use case) leads to incorrectly generated TypeScript types.

Pull request:
Only set unset `additionalProperties` fields to `false`
Closes #2309

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f2dfddec..c421100b1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,11 @@ and this project adheres to
 - cosmwasm-vm: Automatically derive cache version from function hashes and the
   Wasmer version ([#2250])
 
+## Fixed
+
+- cosmwasm-schema: The schema export now doesn't overwrite existing
+  `additionalProperties` values anymore ([#2310])
+
 [#2118]: https://github.com/CosmWasm/cosmwasm/pull/2118
 [#2211]: https://github.com/CosmWasm/cosmwasm/issues/2211
 [#2246]: https://github.com/CosmWasm/cosmwasm/pull/2246
@@ -76,6 +81,7 @@ and this project adheres to
 [#2255]: https://github.com/CosmWasm/cosmwasm/pull/2255
 [#2260]: https://github.com/CosmWasm/cosmwasm/pull/2260
 [#2250]: https://github.com/CosmWasm/cosmwasm/pull/2250
+[#2310]: https://github.com/CosmWasm/cosmwasm/pull/2310
 
 ## [2.1.3] - 2024-08-08
 
diff --git a/Cargo.lock b/Cargo.lock
index a6d809b6f..61ffbb6b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -522,6 +522,18 @@ dependencies = [
  "windows-sys 0.48.0",
 ]
 
+[[package]]
+name = "console"
+version = "0.15.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
+dependencies = [
+ "encode_unicode",
+ "lazy_static",
+ "libc",
+ "windows-sys 0.52.0",
+]
+
 [[package]]
 name = "const-oid"
 version = "0.9.6"
@@ -615,6 +627,7 @@ version = "2.2.0-rc.1"
 dependencies = [
  "anyhow",
  "cosmwasm-schema-derive",
+ "insta",
  "schemars",
  "semver",
  "serde",
@@ -1086,6 +1099,12 @@ dependencies = [
  "zeroize",
 ]
 
+[[package]]
+name = "encode_unicode"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
+
 [[package]]
 name = "english-numbers"
 version = "0.3.3"
@@ -1374,6 +1393,19 @@ dependencies = [
  "hashbrown 0.14.5",
 ]
 
+[[package]]
+name = "insta"
+version = "1.41.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
+dependencies = [
+ "console",
+ "lazy_static",
+ "linked-hash-map",
+ "serde",
+ "similar",
+]
+
 [[package]]
 name = "is-terminal"
 version = "0.4.12"
@@ -1460,6 +1492,12 @@ version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.14"
@@ -2241,6 +2279,12 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
 
+[[package]]
+name = "similar"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e"
+
 [[package]]
 name = "smallvec"
 version = "1.13.2"
diff --git a/packages/schema/Cargo.toml b/packages/schema/Cargo.toml
index 0ea8618e7..54844b3c4 100644
--- a/packages/schema/Cargo.toml
+++ b/packages/schema/Cargo.toml
@@ -19,5 +19,6 @@ thiserror = "1.0.26"
 
 [dev-dependencies]
 anyhow = "1.0.57"
+insta = { version = "1.41.1", features = ["json"] }
 semver = "1"
 tempfile = "3"
diff --git a/packages/schema/src/schema_for.rs b/packages/schema/src/schema_for.rs
index ab36df1c2..a8684b598 100644
--- a/packages/schema/src/schema_for.rs
+++ b/packages/schema/src/schema_for.rs
@@ -30,6 +30,10 @@ macro_rules! schema_for {
                 $crate::schemars::visit::visit_schema_object(self, schema);
 
                 if let Some(ref mut validation) = schema.object {
+                    if validation.additional_properties.is_some() {
+                        return;
+                    }
+
                     validation.additional_properties = Some(Box::new(false.into()));
                 }
             }


Test patch:
diff --git a/packages/schema/tests/idl.rs b/packages/schema/tests/idl.rs
index 57d1a3a0b..490b01b9c 100644
--- a/packages/schema/tests/idl.rs
+++ b/packages/schema/tests/idl.rs
@@ -32,6 +32,18 @@ pub struct MigrateMsg {
     pub cap: u128,
 }
 
+#[cw_serde]
+pub struct MapMsg {
+    btree: std::collections::BTreeMap<String, u32>,
+    hash: std::collections::HashMap<String, u32>,
+}
+
+#[test]
+fn assert_maps_generate_correctly() {
+    let schema = cosmwasm_schema::schema_for!(MapMsg);
+    insta::assert_json_snapshot!(schema);
+}
+
 #[test]
 fn unknown_fields_explicitly_allowed() {
     let json = serde_json::json!({
diff --git a/packages/schema/tests/snapshots/idl__assert_maps_generate_correctly.snap b/packages/schema/tests/snapshots/idl__assert_maps_generate_correctly.snap
new file mode 100644
index 000000000..629148a4e
--- /dev/null
+++ b/packages/schema/tests/snapshots/idl__assert_maps_generate_correctly.snap
@@ -0,0 +1,32 @@
+---
+source: packages/schema/tests/idl.rs
+expression: schema
+---
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "MapMsg",
+  "type": "object",
+  "required": [
+    "btree",
+    "hash"
+  ],
+  "properties": {
+    "btree": {
+      "type": "object",
+      "additionalProperties": {
+        "type": "integer",
+        "format": "uint32",
+        "minimum": 0.0
+      }
+    },
+    "hash": {
+      "type": "object",
+      "additionalProperties": {
+        "type": "integer",
+        "format": "uint32",
+        "minimum": 0.0
+      }
+    }
+  },
+  "additionalProperties": false
+}