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}