Skip to main content

dpp/data_contract/conversion/serde/
mod.rs

1//! Manual `Serialize` / `Deserialize` for the outer `DataContract` enum.
2//!
3//! # Critical-4: platform-version coupling (pinned by tests below)
4//!
5//! Both impls call `PlatformVersion::get_version_or_current_or_latest(None)`,
6//! making serialization output *depend on a process-global thread-local-ish*
7//! state — same DataContract value, different bytes if the active platform
8//! version changes. This is by design: `DataContract` is a versioned enum
9//! routed through `DataContractInSerializationFormat`, and the format depends
10//! on the current platform.
11//!
12//! # Validation policy: validate by default
13//!
14//! The `Deserialize` impl runs **full schema validation** — it decodes the
15//! wire form into `DataContractInSerializationFormat`, then converts it to a
16//! `DataContract` with `full_validation = true`. So
17//! `serde_json::from_value::<DataContract>(...)` / `from_str` reject a
18//! structurally-decodable but schema-invalid contract: a `DataContract` you
19//! can hold is a valid one.
20//!
21//! To reconstruct already-trusted data *without* re-validating it (e.g.
22//! storage reads, round-trips of a known-good value), use the explicit
23//! no-validation path
24//! `DataContractJsonConversionMethodsV0::from_json(value, false, pv)` /
25//! `DataContractValueConversionMethodsV0::from_value(value, false, pv)` — those
26//! call `try_from_platform_versioned` directly and skip schema validation.
27//!
28//! **Why this is KEEP-AS-EXCEPTION**: the manual impls also inject the active
29//! `PlatformVersion` (via `get_version_or_current_or_latest`), coupling the
30//! output to process-global state. This is by design — `DataContract` is a
31//! versioned enum routed through `DataContractInSerializationFormat`, and the
32//! format depends on the current platform. The stateless alternative is the
33//! bincode storage path (`serialize_to_bytes_with_platform_version`).
34//!
35//! The `data_contract_serde_pins_critical_4` test module below pins this
36//! behavior (validate-by-default + explicit `false` opt-out) so future
37//! refactors can't silently change it.
38
39use crate::data_contract::serialized_version::DataContractInSerializationFormat;
40use crate::prelude::DataContract;
41use crate::version::PlatformVersionCurrentVersion;
42use crate::ProtocolError;
43use platform_version::version::PlatformVersion;
44use platform_version::TryIntoPlatformVersioned;
45use serde::{Deserialize, Deserializer, Serialize, Serializer};
46
47impl Serialize for DataContract {
48    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: Serializer,
51    {
52        let current_version = PlatformVersion::get_version_or_current_or_latest(None)
53            .map_err(|e| serde::ser::Error::custom(e.to_string()))?;
54        let data_contract_in_serialization_format: DataContractInSerializationFormat = self
55            .try_into_platform_versioned(current_version)
56            .map_err(|e: ProtocolError| serde::ser::Error::custom(format!("expected to be able to serialize data contract into its serialized version: {}", e)))?;
57        data_contract_in_serialization_format.serialize(serializer)
58    }
59}
60
61impl<'de> Deserialize<'de> for DataContract {
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: Deserializer<'de>,
65    {
66        let serialization_format = DataContractInSerializationFormat::deserialize(deserializer)?;
67        let current_version = PlatformVersion::get_version_or_current_or_latest(None)
68            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
69        // Full schema validation: a deserialized `DataContract` is a valid one.
70        // To reconstruct already-trusted data without re-validating, use the
71        // explicit `from_json`/`from_value(_, false, _)` path instead of serde.
72        // See the module-level doc comment for the rationale.
73        DataContract::try_from_platform_versioned(
74            serialization_format,
75            true,
76            &mut vec![],
77            current_version,
78        )
79        .map_err(serde::de::Error::custom)
80    }
81}
82
83#[cfg(test)]
84mod data_contract_serde_pins_critical_4 {
85    //! Behavior pins for Critical-4 (DataContract serde impurity).
86    //!
87    //! These tests don't fix anything — they snapshot the current behavior
88    //! so a future refactor that quietly changes either the
89    //! `PlatformVersion::get_current()` coupling or the hardcoded
90    //! `full_validation = true` will fail loudly.
91    //!
92    //! See module-level doc above and the unification plan §3.0 Critical-4.
93    use super::*;
94    use crate::data_contract::accessors::v0::DataContractV0Getters;
95    use crate::data_contract::serialized_version::DataContractInSerializationFormat;
96    use crate::tests::fixtures::get_data_contract_fixture;
97    use platform_version::version::LATEST_PLATFORM_VERSION;
98
99    /// PIN: `DataContract` round-trips through `serde_json` at the active
100    /// platform version. Documents that `Serialize` / `Deserialize` are
101    /// load-bearing for JSON-shape interchange (not just bincode).
102    #[test]
103    fn data_contract_round_trips_through_serde_json() {
104        let created = get_data_contract_fixture(None, 0, 1);
105        let original = created.data_contract().clone();
106
107        let json = serde_json::to_value(&original).expect("serialize to json");
108        let recovered: DataContract = serde_json::from_value(json).expect("deserialize from json");
109
110        assert_eq!(original.id(), recovered.id());
111        assert_eq!(original.owner_id(), recovered.owner_id());
112        assert_eq!(original.version(), recovered.version());
113    }
114
115    /// PIN: `DataContract::serialize` produces the same wire shape as
116    /// `DataContractInSerializationFormat::serialize`. This documents that
117    /// the manual impl is a thin wrapper that injects
118    /// `PlatformVersion::get_current()` and forwards to the format type —
119    /// not a custom shape.
120    #[test]
121    fn data_contract_serialize_matches_serialization_format_at_current_version() {
122        let created = get_data_contract_fixture(None, 0, 1);
123        let original = created.data_contract().clone();
124
125        let direct_json = serde_json::to_value(&original).expect("DataContract -> json");
126
127        let format: DataContractInSerializationFormat = original
128            .try_into_platform_versioned(LATEST_PLATFORM_VERSION)
129            .expect("DataContract -> SerializationFormat at latest");
130        let format_json = serde_json::to_value(&format).expect("SerializationFormat -> json");
131
132        assert_eq!(
133            direct_json, format_json,
134            "DataContract::serialize should be byte-equivalent to \
135             DataContractInSerializationFormat::serialize at the current \
136             platform version. If this fails, the manual serde impl has \
137             diverged from the format-routing pattern documented in the \
138             module-level comment."
139        );
140    }
141
142    /// PIN: `DataContract::deserialize` runs **full schema validation** — a
143    /// deserialized `DataContract` is a valid one. The explicit
144    /// `from_json(_, false, _)` path is the opt-out for reconstructing
145    /// already-trusted data without re-validation.
146    ///
147    /// We exercise this with a structurally well-formed payload whose document
148    /// schema is semantically invalid (an `indices` entry referencing a
149    /// nonexistent property):
150    ///
151    /// - canonical `DataContract::deserialize` REJECTS it (validation runs).
152    /// - explicit `from_json(_, false, _)` ACCEPTS it (validation skipped).
153    /// - explicit `from_json(_, true, _)` REJECTS it (validation runs).
154    ///
155    /// If a future refactor flips canonical Deserialize back to not validating,
156    /// this test fails loudly. See module-level doc above for the rationale.
157    #[test]
158    fn data_contract_deserialize_validates_by_default() {
159        use crate::data_contract::conversion::json::DataContractJsonConversionMethodsV0;
160
161        // Build a valid contract, then mutate its JSON to make the schema
162        // semantically invalid: declare an index over a property not in
163        // the schema's `properties` map. Structurally well-formed JSON;
164        // only schema validation catches the issue.
165        let created = get_data_contract_fixture(None, 0, 1);
166        let original = created.data_contract().clone();
167
168        let mut json = serde_json::to_value(&original).expect("to_json");
169
170        let document_schemas = json
171            .get_mut("documentSchemas")
172            .and_then(|v| v.as_object_mut())
173            .expect("documentSchemas object");
174        let (_, first_schema) = document_schemas
175            .iter_mut()
176            .next()
177            .expect("at least one document schema");
178        let schema_obj = first_schema.as_object_mut().expect("schema is object");
179        schema_obj.insert(
180            "indices".to_string(),
181            serde_json::json!([
182                {
183                    "name": "invalid_idx",
184                    "properties": [{"definitelyDoesNotExist": "asc"}],
185                    "unique": false,
186                }
187            ]),
188        );
189
190        // Format-level deserialize succeeds (never validated).
191        let _: DataContractInSerializationFormat = serde_json::from_value(json.clone())
192            .expect("format-level deserialize should accept structurally-valid input");
193
194        // PIN: canonical Deserialize REJECTS the invalid schema (validates).
195        let canonical_result: Result<DataContract, _> = serde_json::from_value(json.clone());
196        assert!(
197            canonical_result.is_err(),
198            "DataContract::deserialize should run schema validation and reject \
199             an invalid index. If this passes, validate-by-default has been \
200             silently reverted."
201        );
202
203        // PIN: explicit opt-out accepts the same payload without validating.
204        let unvalidated = DataContract::from_json(json.clone(), false, LATEST_PLATFORM_VERSION);
205        assert!(
206            unvalidated.is_ok(),
207            "DataContract::from_json(_, false, _) should skip schema validation \
208             and accept the structurally-well-formed payload."
209        );
210
211        // PIN: explicit validated path also rejects.
212        let validated_result = DataContract::from_json(json, true, LATEST_PLATFORM_VERSION);
213        assert!(
214            validated_result.is_err(),
215            "DataContract::from_json(_, true, _) should reject contracts with \
216             invalid indices."
217        );
218    }
219}