Skip to content

Commit f3236ad

Browse files
authored
Merge pull request #2445 from CosmWasm/feature/nullable-denom-metadata-main
[Backport] feat(metadata): Add deserializer to handle null values
2 parents 784ebdc + 58c57b2 commit f3236ad

File tree

2 files changed

+286
-1
lines changed

2 files changed

+286
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ and this project adheres to
7070
- cosmwasm-schema: The schema export now doesn't overwrite existing
7171
`additionalProperties` values anymore ([#2310])
7272
- cosmwasm-vm: Fix CWA-2025-002.
73+
- cosmwasm-std: Fix deserialization of `DenomMetadata`. ([#2417])
7374

7475
[#2141]: https://github.com/CosmWasm/cosmwasm/issues/2141
7576
[#2155]: https://github.com/CosmWasm/cosmwasm/issues/2155
@@ -93,6 +94,7 @@ and this project adheres to
9394
[#2393]: https://github.com/CosmWasm/cosmwasm/issues/2393
9495
[#2399]: https://github.com/CosmWasm/cosmwasm/pull/2399
9596
[#2403]: https://github.com/CosmWasm/cosmwasm/pull/2403
97+
[#2417]: https://github.com/CosmWasm/cosmwasm/pull/2417
9698
[#2432]: https://github.com/CosmWasm/cosmwasm/pull/2432
9799
[#2438]: https://github.com/CosmWasm/cosmwasm/pull/2438
98100

packages/std/src/metadata.rs

Lines changed: 284 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use schemars::JsonSchema;
2-
use serde::{Deserialize, Serialize};
2+
use serde::{Deserialize, Deserializer, Serialize};
33

44
use crate::prelude::*;
55

66
/// Replicates the cosmos-sdk bank module Metadata type
77
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, JsonSchema)]
88
pub struct DenomMetadata {
99
pub description: String,
10+
#[serde(deserialize_with = "deserialize_null_default")]
1011
pub denom_units: Vec<DenomUnit>,
1112
pub base: String,
1213
pub display: String,
@@ -21,5 +22,287 @@ pub struct DenomMetadata {
2122
pub struct DenomUnit {
2223
pub denom: String,
2324
pub exponent: u32,
25+
#[serde(deserialize_with = "deserialize_null_default")]
2426
pub aliases: Vec<String>,
2527
}
28+
29+
// Deserialize a field that is null, defaulting to the type's default value.
30+
// Panic if the field is missing.
31+
fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
32+
where
33+
T: Default + Deserialize<'de>,
34+
D: Deserializer<'de>,
35+
{
36+
let opt = Option::deserialize(deserializer)?;
37+
Ok(opt.unwrap_or_default())
38+
}
39+
40+
#[cfg(test)]
41+
mod tests {
42+
use super::*;
43+
use crate::{DenomMetadata, DenomUnit};
44+
use serde_json::{json, Error};
45+
46+
#[test]
47+
fn deserialize_denom_metadata_with_null_fields_works() {
48+
// Test case with null denom_units - should deserialize as empty vec
49+
let json_with_null_denom_units = json!({
50+
"description": "Test Token",
51+
"denom_units": null,
52+
"base": "utest",
53+
"display": "TEST",
54+
"name": "Test Token",
55+
"symbol": "TEST",
56+
"uri": "https://test.com",
57+
"uri_hash": "hash"
58+
});
59+
60+
let metadata_null_denom_units: DenomMetadata =
61+
serde_json::from_value(json_with_null_denom_units).unwrap();
62+
assert_eq!(
63+
metadata_null_denom_units.denom_units,
64+
Vec::<DenomUnit>::new()
65+
);
66+
67+
// Test normal case with provided denom_units
68+
let json_with_units = json!({
69+
"description": "Test Token",
70+
"denom_units": [
71+
{
72+
"denom": "utest",
73+
"exponent": 6,
74+
"aliases": ["microtest"]
75+
}
76+
],
77+
"base": "utest",
78+
"display": "TEST",
79+
"name": "Test Token",
80+
"symbol": "TEST",
81+
"uri": "https://test.com",
82+
"uri_hash": "hash"
83+
});
84+
85+
let metadata_with_units: DenomMetadata = serde_json::from_value(json_with_units).unwrap();
86+
assert_eq!(metadata_with_units.denom_units.len(), 1);
87+
assert_eq!(metadata_with_units.denom_units[0].denom, "utest");
88+
89+
// Test with null aliases inside denom_units - should deserialize as empty vec
90+
let json_with_null_aliases = json!({
91+
"description": "Test Token",
92+
"denom_units": [
93+
{
94+
"denom": "utest",
95+
"exponent": 6,
96+
"aliases": null
97+
}
98+
],
99+
"base": "utest",
100+
"display": "TEST",
101+
"name": "Test Token",
102+
"symbol": "TEST",
103+
"uri": "https://test.com",
104+
"uri_hash": "hash"
105+
});
106+
107+
let metadata_with_null_aliases: DenomMetadata =
108+
serde_json::from_value(json_with_null_aliases).unwrap();
109+
assert_eq!(metadata_with_null_aliases.denom_units.len(), 1);
110+
assert_eq!(
111+
metadata_with_null_aliases.denom_units[0].aliases,
112+
Vec::<String>::new()
113+
);
114+
}
115+
116+
#[test]
117+
fn deserialize_denom_metadata_with_missing_fields_fails() {
118+
// Missing denom_units should be treated like null
119+
let json_missing_denom_units = json!({
120+
"description": "Test Token",
121+
"base": "utest",
122+
"display": "TEST",
123+
"name": "Test Token",
124+
"symbol": "TEST",
125+
"uri": "https://test.com",
126+
"uri_hash": "hash"
127+
});
128+
129+
let metadata: Result<DenomMetadata, Error> =
130+
serde_json::from_value(json_missing_denom_units);
131+
assert!(metadata.is_err());
132+
133+
let json_missing_alias = json!({
134+
"description": "Test Token",
135+
"base": "utest",
136+
"denom_units": [
137+
{
138+
"denom": "utest",
139+
"exponent": 6,
140+
}
141+
],
142+
"display": "TEST",
143+
"name": "Test Token",
144+
"symbol": "TEST",
145+
"uri": "https://test.com",
146+
"uri_hash": "hash"
147+
});
148+
149+
let metadata_missing_alias: Result<DenomMetadata, Error> =
150+
serde_json::from_value(json_missing_alias);
151+
assert!(metadata_missing_alias.is_err());
152+
}
153+
154+
#[test]
155+
fn query_denom_metadata_with_null_denom_units_works() {
156+
// Test case with null denom_units - should deserialize as empty vec
157+
let json_with_null_denom_units = json!({
158+
"description": "Test Token",
159+
"denom_units": null,
160+
"base": "utest",
161+
"display": "TEST",
162+
"name": "Test Token",
163+
"symbol": "TEST",
164+
"uri": "https://test.com",
165+
"uri_hash": "hash"
166+
});
167+
168+
let metadata_with_null_denom_units: DenomMetadata =
169+
serde_json::from_value(json_with_null_denom_units).unwrap();
170+
assert_eq!(
171+
metadata_with_null_denom_units.denom_units,
172+
Vec::<DenomUnit>::new()
173+
);
174+
175+
// Test normal case with provided denom_units
176+
let json_with_units = json!({
177+
"description": "Test Token",
178+
"denom_units": [
179+
{
180+
"denom": "utest",
181+
"exponent": 6,
182+
"aliases": ["microtest"]
183+
}
184+
],
185+
"base": "utest",
186+
"display": "TEST",
187+
"name": "Test Token",
188+
"symbol": "TEST",
189+
"uri": "https://test.com",
190+
"uri_hash": "hash"
191+
});
192+
193+
let metadata_with_units: DenomMetadata = serde_json::from_value(json_with_units).unwrap();
194+
assert_eq!(metadata_with_units.denom_units.len(), 1);
195+
assert_eq!(metadata_with_units.denom_units[0].denom, "utest");
196+
assert_eq!(metadata_with_units.denom_units[0].aliases.len(), 1);
197+
assert_eq!(metadata_with_units.denom_units[0].aliases[0], "microtest");
198+
199+
// Test with null aliases inside denom_units - should deserialize as empty vec
200+
let json_with_null_aliases = json!({
201+
"description": "Test Token",
202+
"denom_units": [
203+
{
204+
"denom": "utest",
205+
"exponent": 6,
206+
"aliases": null
207+
}
208+
],
209+
"base": "utest",
210+
"display": "TEST",
211+
"name": "Test Token",
212+
"symbol": "TEST",
213+
"uri": "https://test.com",
214+
"uri_hash": "hash"
215+
});
216+
217+
let metadata_with_null_aliases: DenomMetadata =
218+
serde_json::from_value(json_with_null_aliases).unwrap();
219+
assert_eq!(metadata_with_null_aliases.denom_units.len(), 1);
220+
assert_eq!(
221+
metadata_with_null_aliases.denom_units[0].aliases,
222+
Vec::<String>::new()
223+
);
224+
}
225+
226+
#[test]
227+
fn query_denom_metadata_with_missing_fields_fails() {
228+
// Missing denom_units should throw an error
229+
let json_missing_denom_units = json!({
230+
"description": "Test Token",
231+
"base": "utest",
232+
"display": "TEST",
233+
"name": "Test Token",
234+
"symbol": "TEST",
235+
"uri": "https://test.com",
236+
"uri_hash": "hash"
237+
});
238+
239+
let json_missing_denom_units_metadata: Result<DenomMetadata, Error> =
240+
serde_json::from_value(json_missing_denom_units);
241+
assert!(json_missing_denom_units_metadata.is_err());
242+
243+
// Missing aliases field should throw an error
244+
let json_missing_aliases = json!({
245+
"description": "Test Token",
246+
"denom_units": [
247+
{
248+
"denom": "utest",
249+
"exponent": 6
250+
}
251+
],
252+
"base": "utest",
253+
"display": "TEST",
254+
"name": "Test Token",
255+
"symbol": "TEST",
256+
"uri": "https://test.com",
257+
"uri_hash": "hash"
258+
});
259+
260+
let missing_aliases_metadata: Result<DenomMetadata, Error> =
261+
serde_json::from_value(json_missing_aliases);
262+
assert!(missing_aliases_metadata.is_err());
263+
}
264+
265+
#[test]
266+
fn query_denom_metadata_with_mixed_null_and_value_works() {
267+
// Test with multiple denom units, some with null aliases and some with values
268+
let mixed_json = json!({
269+
"description": "Mixed Token",
270+
"denom_units": [
271+
{
272+
"denom": "unit1",
273+
"exponent": 0,
274+
"aliases": null
275+
},
276+
{
277+
"denom": "unit2",
278+
"exponent": 6,
279+
"aliases": ["microunit", "u"]
280+
},
281+
{
282+
"denom": "unit3",
283+
"exponent": 9,
284+
"aliases": []
285+
}
286+
],
287+
"base": "unit1",
288+
"display": "MIXED",
289+
"name": "Mixed Token",
290+
"symbol": "MIX",
291+
"uri": "https://mixed.token",
292+
"uri_hash": "hash123"
293+
});
294+
295+
let metadata: DenomMetadata = serde_json::from_value(mixed_json).unwrap();
296+
297+
// First denom unit has null aliases, should be empty vec
298+
assert!(metadata.denom_units[0].aliases.is_empty());
299+
300+
// Second has two aliases
301+
assert_eq!(metadata.denom_units[1].aliases.len(), 2);
302+
assert_eq!(metadata.denom_units[1].aliases[0], "microunit");
303+
assert_eq!(metadata.denom_units[1].aliases[1], "u");
304+
305+
// Third has explicitly empty aliases
306+
assert!(metadata.denom_units[2].aliases.is_empty());
307+
}
308+
}

0 commit comments

Comments
 (0)