dpp/document/v0/
json_conversion.rs

1use crate::document::fields::property_names;
2use crate::document::serialization_traits::{
3    DocumentJsonMethodsV0, DocumentPlatformValueMethodsV0,
4};
5use crate::document::DocumentV0;
6use crate::util::json_value::JsonValueExt;
7use crate::ProtocolError;
8use platform_value::{Identifier, Value};
9use platform_version::version::PlatformVersion;
10use serde::Deserialize;
11use serde_json::{json, Value as JsonValue};
12use std::convert::TryInto;
13
14impl DocumentJsonMethodsV0<'_> for DocumentV0 {
15    fn to_json_with_identifiers_using_bytes(
16        &self,
17        _platform_version: &PlatformVersion,
18    ) -> Result<JsonValue, ProtocolError> {
19        let mut value = json!({
20            property_names::ID: self.id,
21            property_names::OWNER_ID: self.owner_id,
22        });
23        let value_mut = value.as_object_mut().unwrap();
24        if let Some(created_at) = self.created_at {
25            value_mut.insert(
26                property_names::CREATED_AT.to_string(),
27                JsonValue::Number(created_at.into()),
28            );
29        }
30        if let Some(updated_at) = self.updated_at {
31            value_mut.insert(
32                property_names::UPDATED_AT.to_string(),
33                JsonValue::Number(updated_at.into()),
34            );
35        }
36        if let Some(created_at_block_height) = self.created_at_block_height {
37            value_mut.insert(
38                property_names::CREATED_AT_BLOCK_HEIGHT.to_string(),
39                JsonValue::Number(created_at_block_height.into()),
40            );
41        }
42
43        if let Some(updated_at_block_height) = self.updated_at_block_height {
44            value_mut.insert(
45                property_names::UPDATED_AT_BLOCK_HEIGHT.to_string(),
46                JsonValue::Number(updated_at_block_height.into()),
47            );
48        }
49
50        if let Some(created_at_core_block_height) = self.created_at_core_block_height {
51            value_mut.insert(
52                property_names::CREATED_AT_CORE_BLOCK_HEIGHT.to_string(),
53                JsonValue::Number(created_at_core_block_height.into()),
54            );
55        }
56
57        if let Some(updated_at_core_block_height) = self.updated_at_core_block_height {
58            value_mut.insert(
59                property_names::UPDATED_AT_CORE_BLOCK_HEIGHT.to_string(),
60                JsonValue::Number(updated_at_core_block_height.into()),
61            );
62        }
63        if let Some(transferred_at) = self.transferred_at {
64            value_mut.insert(
65                property_names::TRANSFERRED_AT.to_string(),
66                JsonValue::Number(transferred_at.into()),
67            );
68        }
69        if let Some(transferred_at_block_height) = self.transferred_at_block_height {
70            value_mut.insert(
71                property_names::TRANSFERRED_AT_BLOCK_HEIGHT.to_string(),
72                JsonValue::Number(transferred_at_block_height.into()),
73            );
74        }
75        if let Some(transferred_at_core_block_height) = self.transferred_at_core_block_height {
76            value_mut.insert(
77                property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT.to_string(),
78                JsonValue::Number(transferred_at_core_block_height.into()),
79            );
80        }
81        if let Some(creator_id) = self.creator_id {
82            value_mut.insert(property_names::CREATOR_ID.to_string(), json!(creator_id));
83        }
84        if let Some(revision) = self.revision {
85            value_mut.insert(
86                property_names::REVISION.to_string(),
87                JsonValue::Number(revision.into()),
88            );
89        }
90
91        self.properties
92            .iter()
93            .try_for_each(|(key, property_value)| {
94                let serde_value: JsonValue = property_value.try_to_validating_json()?;
95                value_mut.insert(key.to_string(), serde_value);
96                Ok::<(), ProtocolError>(())
97            })?;
98
99        Ok(value)
100    }
101
102    fn to_json(&self, _platform_version: &PlatformVersion) -> Result<JsonValue, ProtocolError> {
103        self.to_object()
104            .map(|v| v.try_into().map_err(ProtocolError::ValueError))?
105    }
106
107    fn from_json_value<S, E>(
108        mut document_value: JsonValue,
109        _platform_version: &PlatformVersion,
110    ) -> Result<Self, ProtocolError>
111    where
112        for<'de> S: Deserialize<'de> + TryInto<Identifier, Error = E>,
113        E: Into<ProtocolError>,
114    {
115        let mut document = Self {
116            ..Default::default()
117        };
118
119        if let Ok(value) = document_value.remove(property_names::ID) {
120            if !value.is_null() {
121                let data: S = serde_json::from_value(value)?;
122                document.id = data.try_into().map_err(Into::into)?;
123            }
124        }
125        if let Ok(value) = document_value.remove(property_names::OWNER_ID) {
126            if !value.is_null() {
127                let data: S = serde_json::from_value(value)?;
128                document.owner_id = data.try_into().map_err(Into::into)?;
129            }
130        }
131        if let Ok(value) = document_value.remove(property_names::REVISION) {
132            document.revision = serde_json::from_value(value)?
133        }
134        if let Ok(value) = document_value.remove(property_names::CREATED_AT) {
135            document.created_at = serde_json::from_value(value)?
136        }
137        if let Ok(value) = document_value.remove(property_names::UPDATED_AT) {
138            document.updated_at = serde_json::from_value(value)?
139        }
140        if let Ok(value) = document_value.remove(property_names::CREATED_AT_BLOCK_HEIGHT) {
141            document.created_at_block_height = serde_json::from_value(value)?;
142        }
143        if let Ok(value) = document_value.remove(property_names::UPDATED_AT_BLOCK_HEIGHT) {
144            document.updated_at_block_height = serde_json::from_value(value)?;
145        }
146        if let Ok(value) = document_value.remove(property_names::CREATED_AT_CORE_BLOCK_HEIGHT) {
147            document.created_at_core_block_height = serde_json::from_value(value)?;
148        }
149        if let Ok(value) = document_value.remove(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT) {
150            document.updated_at_core_block_height = serde_json::from_value(value)?;
151        }
152        if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT) {
153            document.transferred_at = serde_json::from_value(value)?;
154        }
155        if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT_BLOCK_HEIGHT) {
156            document.transferred_at_block_height = serde_json::from_value(value)?;
157        }
158        if let Ok(value) = document_value.remove(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT) {
159            document.transferred_at_core_block_height = serde_json::from_value(value)?;
160        }
161        if let Ok(value) = document_value.remove(property_names::CREATOR_ID) {
162            if !value.is_null() {
163                let data: S = serde_json::from_value(value)?;
164                document.creator_id = Some(data.try_into().map_err(Into::into)?);
165            }
166        }
167
168        let platform_value: Value = document_value.into();
169
170        document.properties = platform_value
171            .into_btree_string_map()
172            .map_err(ProtocolError::ValueError)?;
173        Ok(document)
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::data_contract::accessors::v0::DataContractV0Getters;
181    use crate::data_contract::document_type::random_document::CreateRandomDocument;
182    use crate::document::serialization_traits::DocumentJsonMethodsV0;
183    use crate::tests::json_document::json_document_to_contract;
184    use platform_version::version::PlatformVersion;
185    use std::collections::BTreeMap;
186
187    fn make_document_v0_with_all_timestamps() -> DocumentV0 {
188        let mut properties = BTreeMap::new();
189        properties.insert("label".to_string(), Value::Text("test-label".to_string()));
190        DocumentV0 {
191            id: Identifier::new([1u8; 32]),
192            owner_id: Identifier::new([2u8; 32]),
193            properties,
194            revision: Some(3),
195            created_at: Some(1_700_000_000_000),
196            updated_at: Some(1_700_000_100_000),
197            transferred_at: Some(1_700_000_200_000),
198            created_at_block_height: Some(100),
199            updated_at_block_height: Some(200),
200            transferred_at_block_height: Some(300),
201            created_at_core_block_height: Some(50),
202            updated_at_core_block_height: Some(60),
203            transferred_at_core_block_height: Some(70),
204            creator_id: Some(Identifier::new([9u8; 32])),
205        }
206    }
207
208    fn make_minimal_document_v0() -> DocumentV0 {
209        DocumentV0 {
210            id: Identifier::new([0xAA; 32]),
211            owner_id: Identifier::new([0xBB; 32]),
212            properties: BTreeMap::new(),
213            revision: None,
214            created_at: None,
215            updated_at: None,
216            transferred_at: None,
217            created_at_block_height: None,
218            updated_at_block_height: None,
219            transferred_at_block_height: None,
220            created_at_core_block_height: None,
221            updated_at_core_block_height: None,
222            transferred_at_core_block_height: None,
223            creator_id: None,
224        }
225    }
226
227    // ================================================================
228    //  to_json produces a JsonValue containing all set fields
229    // ================================================================
230
231    #[test]
232    fn to_json_includes_id_and_owner_id() {
233        let platform_version = PlatformVersion::latest();
234        let doc = make_minimal_document_v0();
235        let json = doc
236            .to_json(platform_version)
237            .expect("to_json should succeed");
238        let obj = json.as_object().expect("should be an object");
239        assert!(
240            obj.contains_key(property_names::ID),
241            "JSON should contain $id"
242        );
243        assert!(
244            obj.contains_key(property_names::OWNER_ID),
245            "JSON should contain $ownerId"
246        );
247    }
248
249    #[test]
250    fn to_json_represents_none_timestamps_as_null() {
251        let platform_version = PlatformVersion::latest();
252        let doc = make_minimal_document_v0();
253        let json = doc
254            .to_json(platform_version)
255            .expect("to_json should succeed");
256        let obj = json.as_object().expect("should be an object");
257
258        // to_json serializes via serde, so None fields appear as null
259        if let Some(val) = obj.get(property_names::CREATED_AT) {
260            assert!(
261                val.is_null(),
262                "$createdAt should be null when None, got: {:?}",
263                val
264            );
265        }
266        if let Some(val) = obj.get(property_names::UPDATED_AT) {
267            assert!(
268                val.is_null(),
269                "$updatedAt should be null when None, got: {:?}",
270                val
271            );
272        }
273        if let Some(val) = obj.get(property_names::REVISION) {
274            assert!(
275                val.is_null(),
276                "$revision should be null when None, got: {:?}",
277                val
278            );
279        }
280    }
281
282    // ================================================================
283    //  to_json_with_identifiers_using_bytes includes all timestamps
284    // ================================================================
285
286    #[test]
287    fn to_json_with_identifiers_using_bytes_includes_all_timestamp_fields() {
288        let platform_version = PlatformVersion::latest();
289        let doc = make_document_v0_with_all_timestamps();
290        let json = doc
291            .to_json_with_identifiers_using_bytes(platform_version)
292            .expect("to_json_with_identifiers_using_bytes should succeed");
293        let obj = json.as_object().expect("should be an object");
294
295        assert!(obj.contains_key(property_names::ID));
296        assert!(obj.contains_key(property_names::OWNER_ID));
297        assert!(obj.contains_key(property_names::REVISION));
298        assert!(obj.contains_key(property_names::CREATED_AT));
299        assert!(obj.contains_key(property_names::UPDATED_AT));
300        assert!(obj.contains_key(property_names::TRANSFERRED_AT));
301        assert!(obj.contains_key(property_names::CREATED_AT_BLOCK_HEIGHT));
302        assert!(obj.contains_key(property_names::UPDATED_AT_BLOCK_HEIGHT));
303        assert!(obj.contains_key(property_names::TRANSFERRED_AT_BLOCK_HEIGHT));
304        assert!(obj.contains_key(property_names::CREATED_AT_CORE_BLOCK_HEIGHT));
305        assert!(obj.contains_key(property_names::UPDATED_AT_CORE_BLOCK_HEIGHT));
306        assert!(obj.contains_key(property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT));
307        assert!(obj.contains_key(property_names::CREATOR_ID));
308
309        // Verify numeric values
310        assert_eq!(obj[property_names::REVISION].as_u64(), Some(3));
311        assert_eq!(
312            obj[property_names::CREATED_AT].as_u64(),
313            Some(1_700_000_000_000)
314        );
315        assert_eq!(
316            obj[property_names::UPDATED_AT].as_u64(),
317            Some(1_700_000_100_000)
318        );
319        assert_eq!(
320            obj[property_names::TRANSFERRED_AT].as_u64(),
321            Some(1_700_000_200_000)
322        );
323        assert_eq!(
324            obj[property_names::CREATED_AT_BLOCK_HEIGHT].as_u64(),
325            Some(100)
326        );
327        assert_eq!(
328            obj[property_names::UPDATED_AT_BLOCK_HEIGHT].as_u64(),
329            Some(200)
330        );
331        assert_eq!(
332            obj[property_names::TRANSFERRED_AT_BLOCK_HEIGHT].as_u64(),
333            Some(300)
334        );
335        assert_eq!(
336            obj[property_names::CREATED_AT_CORE_BLOCK_HEIGHT].as_u64(),
337            Some(50)
338        );
339        assert_eq!(
340            obj[property_names::UPDATED_AT_CORE_BLOCK_HEIGHT].as_u64(),
341            Some(60)
342        );
343        assert_eq!(
344            obj[property_names::TRANSFERRED_AT_CORE_BLOCK_HEIGHT].as_u64(),
345            Some(70)
346        );
347    }
348
349    #[test]
350    fn to_json_with_identifiers_using_bytes_includes_custom_properties() {
351        let platform_version = PlatformVersion::latest();
352        let doc = make_document_v0_with_all_timestamps();
353        let json = doc
354            .to_json_with_identifiers_using_bytes(platform_version)
355            .expect("should succeed");
356        let obj = json.as_object().expect("should be an object");
357        assert_eq!(
358            obj.get("label").and_then(|v| v.as_str()),
359            Some("test-label")
360        );
361    }
362
363    // ================================================================
364    //  from_json_value round-trip: to_json -> from_json_value
365    //  Uses String as the identifier deserialization type since
366    //  to_json produces base58 string identifiers.
367    // ================================================================
368
369    #[test]
370    fn json_round_trip_with_random_dashpay_profile() {
371        let platform_version = PlatformVersion::latest();
372        let contract = json_document_to_contract(
373            "../rs-drive/tests/supporting_files/contract/dashpay/dashpay-contract.json",
374            false,
375            platform_version,
376        )
377        .expect("expected to load dashpay contract");
378
379        let document_type = contract
380            .document_type_for_name("profile")
381            .expect("expected profile document type");
382
383        for seed in 0..5u64 {
384            let document = document_type
385                .random_document(Some(seed), platform_version)
386                .expect("expected random document");
387
388            let doc_v0 = match &document {
389                crate::document::Document::V0(d) => d,
390            };
391
392            let json_val = doc_v0
393                .to_json(platform_version)
394                .expect("to_json should succeed");
395
396            let recovered = DocumentV0::from_json_value::<String, _>(json_val, platform_version)
397                .expect("from_json_value should succeed");
398
399            assert_eq!(doc_v0.id, recovered.id, "id mismatch for seed {seed}");
400            assert_eq!(
401                doc_v0.owner_id, recovered.owner_id,
402                "owner_id mismatch for seed {seed}"
403            );
404            assert_eq!(
405                doc_v0.revision, recovered.revision,
406                "revision mismatch for seed {seed}"
407            );
408        }
409    }
410
411    // ================================================================
412    //  from_json_value extracts all system fields correctly
413    // ================================================================
414
415    #[test]
416    fn from_json_value_extracts_timestamps_and_revision() {
417        let platform_version = PlatformVersion::latest();
418        let id = Identifier::new([1u8; 32]);
419        let owner = Identifier::new([2u8; 32]);
420        let creator = Identifier::new([9u8; 32]);
421
422        let json_val = json!({
423            "$id": bs58::encode(id.to_buffer()).into_string(),
424            "$ownerId": bs58::encode(owner.to_buffer()).into_string(),
425            "$revision": 5,
426            "$createdAt": 1_000_000u64,
427            "$updatedAt": 2_000_000u64,
428            "$createdAtBlockHeight": 100u64,
429            "$updatedAtBlockHeight": 200u64,
430            "$createdAtCoreBlockHeight": 50u32,
431            "$updatedAtCoreBlockHeight": 60u32,
432            "$transferredAt": 3_000_000u64,
433            "$transferredAtBlockHeight": 300u64,
434            "$transferredAtCoreBlockHeight": 70u32,
435            "$creatorId": bs58::encode(creator.to_buffer()).into_string(),
436            "customProp": "hello"
437        });
438
439        let doc = DocumentV0::from_json_value::<String, _>(json_val, platform_version)
440            .expect("from_json_value should succeed");
441
442        assert_eq!(doc.id, id);
443        assert_eq!(doc.owner_id, owner);
444        assert_eq!(doc.revision, Some(5));
445        assert_eq!(doc.created_at, Some(1_000_000));
446        assert_eq!(doc.updated_at, Some(2_000_000));
447        assert_eq!(doc.created_at_block_height, Some(100));
448        assert_eq!(doc.updated_at_block_height, Some(200));
449        assert_eq!(doc.created_at_core_block_height, Some(50));
450        assert_eq!(doc.updated_at_core_block_height, Some(60));
451        assert_eq!(doc.transferred_at, Some(3_000_000));
452        assert_eq!(doc.transferred_at_block_height, Some(300));
453        assert_eq!(doc.transferred_at_core_block_height, Some(70));
454        assert_eq!(doc.creator_id, Some(creator));
455        // Custom property should be in properties map
456        assert_eq!(
457            doc.properties.get("customProp"),
458            Some(&Value::Text("hello".to_string()))
459        );
460    }
461
462    #[test]
463    fn from_json_value_handles_missing_optional_fields() {
464        let platform_version = PlatformVersion::latest();
465        let id = Identifier::new([3u8; 32]);
466        let owner = Identifier::new([4u8; 32]);
467        let json_val = json!({
468            "$id": bs58::encode(id.to_buffer()).into_string(),
469            "$ownerId": bs58::encode(owner.to_buffer()).into_string(),
470        });
471
472        let doc = DocumentV0::from_json_value::<String, _>(json_val, platform_version)
473            .expect("from_json_value should succeed with minimal fields");
474
475        assert_eq!(doc.id, id);
476        assert_eq!(doc.owner_id, owner);
477        assert_eq!(doc.revision, None);
478        assert_eq!(doc.created_at, None);
479        assert_eq!(doc.updated_at, None);
480        assert_eq!(doc.transferred_at, None);
481        assert_eq!(doc.created_at_block_height, None);
482        assert_eq!(doc.updated_at_block_height, None);
483        assert_eq!(doc.transferred_at_block_height, None);
484        assert_eq!(doc.created_at_core_block_height, None);
485        assert_eq!(doc.updated_at_core_block_height, None);
486        assert_eq!(doc.transferred_at_core_block_height, None);
487        assert_eq!(doc.creator_id, None);
488    }
489
490    // ================================================================
491    //  from_json_value with creator_id
492    // ================================================================
493
494    #[test]
495    fn from_json_value_parses_creator_id() {
496        let platform_version = PlatformVersion::latest();
497        let creator = Identifier::new([0xCC; 32]);
498        let json_val = json!({
499            "$id": bs58::encode([1u8; 32]).into_string(),
500            "$ownerId": bs58::encode([2u8; 32]).into_string(),
501            "$creatorId": bs58::encode(creator.to_buffer()).into_string(),
502        });
503
504        let doc = DocumentV0::from_json_value::<String, _>(json_val, platform_version)
505            .expect("from_json_value with creator_id should succeed");
506
507        assert_eq!(doc.creator_id, Some(creator));
508    }
509}