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