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}