polyxml 0.12.0__tar.gz → 0.13.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {polyxml-0.12.0 → polyxml-0.13.0}/Cargo.lock +6 -5
  2. {polyxml-0.12.0 → polyxml-0.13.0}/Cargo.toml +1 -1
  3. {polyxml-0.12.0 → polyxml-0.13.0}/PKG-INFO +1 -1
  4. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/Cargo.toml +1 -0
  5. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/python/mod.rs +17 -1
  6. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/error.rs +3 -0
  7. polyxml-0.13.0/crates/polyxml-core/src/json.rs +356 -0
  8. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/lib.rs +2 -0
  9. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/value.rs +23 -0
  10. polyxml-0.13.0/crates/polyxml-core/tests/test_json.rs +223 -0
  11. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_python_codegen.rs +7 -0
  12. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/src/lib.rs +40 -0
  13. polyxml-0.13.0/crates/polyxml-python/tests/test_json.py +275 -0
  14. {polyxml-0.12.0 → polyxml-0.13.0}/pyproject.toml +1 -1
  15. {polyxml-0.12.0 → polyxml-0.13.0}/python/polyxml/__init__.py +159 -1
  16. polyxml-0.13.0/python/polyxml/compat/__init__.py +1 -0
  17. polyxml-0.13.0/python/polyxml/compat/xsdata.py +36 -0
  18. {polyxml-0.12.0 → polyxml-0.13.0}/README.md +0 -0
  19. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/README.md +0 -0
  20. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
  21. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/cpp/mod.rs +0 -0
  22. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/csharp/mod.rs +0 -0
  23. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/go/mod.rs +0 -0
  24. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/java/mod.rs +0 -0
  25. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/mod.rs +0 -0
  26. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/rust/mod.rs +0 -0
  27. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/codegen/typescript/mod.rs +0 -0
  28. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/converters.rs +0 -0
  29. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/ir/mod.rs +0 -0
  30. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/ir/tarjan.rs +0 -0
  31. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/parser.rs +0 -0
  32. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/schema.rs +0 -0
  33. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/schema_parser/mod.rs +0 -0
  34. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/src/serializer.rs +0 -0
  35. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_conformance.rs +0 -0
  36. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_core.rs +0 -0
  37. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_cpp_codegen.rs +0 -0
  38. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_csharp_codegen.rs +0 -0
  39. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_go_codegen.rs +0 -0
  40. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_java_codegen.rs +0 -0
  41. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_rust_codecs.rs +0 -0
  42. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_rust_codegen.rs +0 -0
  43. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_schema_ir.rs +0 -0
  44. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_security.rs +0 -0
  45. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-core/tests/test_ts_codegen.rs +0 -0
  46. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/Cargo.toml +0 -0
  47. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/README.md +0 -0
  48. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/tests/test_conformance.py +0 -0
  49. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/tests/test_generated_models.py +0 -0
  50. {polyxml-0.12.0 → polyxml-0.13.0}/crates/polyxml-python/tests/test_polyxml.py +0 -0
  51. {polyxml-0.12.0 → polyxml-0.13.0}/python/polyxml/AGENT_GUIDE.md +0 -0
  52. {polyxml-0.12.0 → polyxml-0.13.0}/python/polyxml/py.typed +0 -0
@@ -549,7 +549,7 @@ dependencies = [
549
549
 
550
550
  [[package]]
551
551
  name = "polyxml"
552
- version = "0.12.0"
552
+ version = "0.13.0"
553
553
  dependencies = [
554
554
  "criterion",
555
555
  "heck",
@@ -559,6 +559,7 @@ dependencies = [
559
559
  "pretty_assertions",
560
560
  "quick-xml",
561
561
  "serde",
562
+ "serde_json",
562
563
  "smallvec",
563
564
  "tempfile",
564
565
  "thiserror",
@@ -566,14 +567,14 @@ dependencies = [
566
567
 
567
568
  [[package]]
568
569
  name = "polyxml-c"
569
- version = "0.12.0"
570
+ version = "0.13.0"
570
571
  dependencies = [
571
572
  "polyxml",
572
573
  ]
573
574
 
574
575
  [[package]]
575
576
  name = "polyxml-cli"
576
- version = "0.12.0"
577
+ version = "0.13.0"
577
578
  dependencies = [
578
579
  "clap",
579
580
  "glob",
@@ -587,7 +588,7 @@ dependencies = [
587
588
 
588
589
  [[package]]
589
590
  name = "polyxml-js"
590
- version = "0.12.0"
591
+ version = "0.13.0"
591
592
  dependencies = [
592
593
  "napi",
593
594
  "napi-build",
@@ -597,7 +598,7 @@ dependencies = [
597
598
 
598
599
  [[package]]
599
600
  name = "polyxml-python"
600
- version = "0.12.0"
601
+ version = "0.13.0"
601
602
  dependencies = [
602
603
  "lexical-core",
603
604
  "polyxml",
@@ -3,7 +3,7 @@ resolver = "2"
3
3
  members = ["crates/polyxml-core", "crates/polyxml-python"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.12.0"
6
+ version = "0.13.0"
7
7
  edition = "2021"
8
8
  rust-version = "1.80"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyxml
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -17,6 +17,7 @@ memchr = "2.8"
17
17
  thiserror = "2.0"
18
18
  smallvec = "1.16"
19
19
  serde = { version = "1.0", features = ["derive"] }
20
+ serde_json = "1.0"
20
21
  minijinja = { version = "2.8", features = ["builtins"] }
21
22
  heck = "0.5"
22
23
 
@@ -529,7 +529,23 @@ impl PythonCodegen {
529
529
  out.push_str(" ) -> bytes:\n");
530
530
  out.push_str(" \"\"\"Serialize this model instance into XML bytes.\"\"\"\n");
531
531
  out.push_str(" import polyxml\n");
532
- out.push_str(" return polyxml.serialize(self, indent=indent, namespaces=namespaces, ns_map=ns_map)\n");
532
+ out.push_str(" return polyxml.serialize(self, indent=indent, namespaces=namespaces, ns_map=ns_map)\n\n");
533
+ out.push_str(" @classmethod\n");
534
+ out.push_str(" def from_json(cls, data: bytes | str) -> Self:\n");
535
+ out.push_str(" \"\"\"Deserialize JSON bytes or string into this model.\"\"\"\n");
536
+ out.push_str(" import polyxml\n");
537
+ out.push_str(" return polyxml.deserialize_json(data, cls)\n\n");
538
+ out.push_str(" def to_json(\n");
539
+ out.push_str(" self,\n");
540
+ out.push_str(" *,\n");
541
+ out.push_str(" indent: int | None = None,\n");
542
+ out.push_str(" by_alias: bool = True,\n");
543
+ out.push_str(" ) -> bytes:\n");
544
+ out.push_str(" \"\"\"Serialize this model instance into JSON bytes.\"\"\"\n");
545
+ out.push_str(" import polyxml\n");
546
+ out.push_str(
547
+ " return polyxml.serialize_json(self, indent=indent, by_alias=by_alias)\n",
548
+ );
533
549
  has_body = true;
534
550
  }
535
551
 
@@ -49,6 +49,9 @@ pub enum PolyXmlError {
49
49
 
50
50
  #[error("I/O error: {0}")]
51
51
  IoError(#[from] std::io::Error),
52
+
53
+ #[error("JSON error: {0}")]
54
+ JsonError(#[from] serde_json::Error),
52
55
  }
53
56
 
54
57
  pub type Result<T> = std::result::Result<T, PolyXmlError>;
@@ -0,0 +1,356 @@
1
+ use std::collections::HashMap;
2
+ use std::sync::Arc;
3
+
4
+ use serde::Serialize;
5
+ use serde_json::Value as JsonValue;
6
+
7
+ use crate::error::Result;
8
+ use crate::schema::{ModelSchema, ScalarType, ValueType};
9
+ use crate::value::PolyValue;
10
+
11
+ /// Convert a `PolyValue` into a `serde_json::Value` dynamically without a schema.
12
+ impl From<&PolyValue> for JsonValue {
13
+ fn from(val: &PolyValue) -> Self {
14
+ match val {
15
+ PolyValue::Null => JsonValue::Null,
16
+ PolyValue::Bool(b) => JsonValue::Bool(*b),
17
+ PolyValue::Int(i) => JsonValue::Number((*i).into()),
18
+ PolyValue::Float(f) => serde_json::Number::from_f64(*f)
19
+ .map(JsonValue::Number)
20
+ .unwrap_or(JsonValue::Null),
21
+ PolyValue::String(s) => JsonValue::String(s.clone()),
22
+ PolyValue::List(l) => JsonValue::Array(l.iter().map(JsonValue::from).collect()),
23
+ PolyValue::Object(m) => {
24
+ let mut map = serde_json::Map::with_capacity(m.len());
25
+ for (k, v) in m {
26
+ map.insert(k.clone(), JsonValue::from(v));
27
+ }
28
+ JsonValue::Object(map)
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ /// Convert a `serde_json::Value` into a `PolyValue` dynamically without a schema.
35
+ impl From<JsonValue> for PolyValue {
36
+ fn from(val: JsonValue) -> Self {
37
+ match val {
38
+ JsonValue::Null => PolyValue::Null,
39
+ JsonValue::Bool(b) => PolyValue::Bool(b),
40
+ JsonValue::Number(n) => {
41
+ if let Some(i) = n.as_i64() {
42
+ PolyValue::Int(i)
43
+ } else if let Some(u) = n.as_u64() {
44
+ PolyValue::Int(u as i64)
45
+ } else if let Some(f) = n.as_f64() {
46
+ PolyValue::Float(f)
47
+ } else {
48
+ PolyValue::String(n.to_string())
49
+ }
50
+ }
51
+ JsonValue::String(s) => PolyValue::String(s),
52
+ JsonValue::Array(a) => PolyValue::List(a.into_iter().map(PolyValue::from).collect()),
53
+ JsonValue::Object(o) => {
54
+ let mut map = HashMap::with_capacity(o.len());
55
+ for (k, v) in o {
56
+ map.insert(k, PolyValue::from(v));
57
+ }
58
+ PolyValue::Object(map)
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ /// Convert a `PolyValue` to `serde_json::Value` guided by a `ModelSchema`.
65
+ pub fn poly_value_to_json_value(
66
+ val: &PolyValue,
67
+ schema: &ModelSchema,
68
+ by_alias: bool,
69
+ ) -> JsonValue {
70
+ match val {
71
+ PolyValue::Object(map) => {
72
+ let mut json_map = serde_json::Map::with_capacity(schema.fields.len());
73
+ for field in &schema.fields {
74
+ let key = if by_alias {
75
+ std::str::from_utf8(&field.xml_name).unwrap_or(field.name.as_str())
76
+ } else {
77
+ field.name.as_str()
78
+ };
79
+
80
+ if let Some(field_val) = map.get(&field.name) {
81
+ let converted = match &field.val_type {
82
+ ValueType::Nested(nested_schema) => {
83
+ if field_val.is_null() {
84
+ JsonValue::Null
85
+ } else {
86
+ poly_value_to_json_value(field_val, nested_schema, by_alias)
87
+ }
88
+ }
89
+ ValueType::List(inner) => {
90
+ if let PolyValue::List(items) = field_val {
91
+ if let ValueType::Nested(nested_schema) = inner.as_ref() {
92
+ JsonValue::Array(
93
+ items
94
+ .iter()
95
+ .map(|item| {
96
+ poly_value_to_json_value(
97
+ item,
98
+ nested_schema,
99
+ by_alias,
100
+ )
101
+ })
102
+ .collect(),
103
+ )
104
+ } else {
105
+ JsonValue::Array(items.iter().map(JsonValue::from).collect())
106
+ }
107
+ } else if field_val.is_null() {
108
+ JsonValue::Null
109
+ } else {
110
+ JsonValue::Array(vec![JsonValue::from(field_val)])
111
+ }
112
+ }
113
+ ValueType::Scalar(_) => JsonValue::from(field_val),
114
+ };
115
+ json_map.insert(key.to_string(), converted);
116
+ }
117
+ }
118
+ JsonValue::Object(json_map)
119
+ }
120
+ other => JsonValue::from(other),
121
+ }
122
+ }
123
+
124
+ /// Coerce a JSON value into a `PolyValue` scalar matching `ScalarType`.
125
+ fn coerce_scalar(val: &JsonValue, st: &ScalarType) -> PolyValue {
126
+ match val {
127
+ JsonValue::Null => PolyValue::Null,
128
+ JsonValue::Bool(b) => PolyValue::Bool(*b),
129
+ JsonValue::Number(n) => match st {
130
+ ScalarType::Int => {
131
+ if let Some(i) = n.as_i64() {
132
+ PolyValue::Int(i)
133
+ } else {
134
+ PolyValue::Int(n.as_f64().unwrap_or(0.0) as i64)
135
+ }
136
+ }
137
+ ScalarType::Float => PolyValue::Float(n.as_f64().unwrap_or(0.0)),
138
+ ScalarType::Decimal | ScalarType::String => PolyValue::String(n.to_string()),
139
+ _ => {
140
+ if let Some(i) = n.as_i64() {
141
+ PolyValue::Int(i)
142
+ } else {
143
+ PolyValue::Float(n.as_f64().unwrap_or(0.0))
144
+ }
145
+ }
146
+ },
147
+ JsonValue::String(s) => match st {
148
+ ScalarType::Int => {
149
+ if let Ok(i) = s.trim().parse::<i64>() {
150
+ PolyValue::Int(i)
151
+ } else {
152
+ PolyValue::String(s.clone())
153
+ }
154
+ }
155
+ ScalarType::Float => {
156
+ if let Ok(f) = s.trim().parse::<f64>() {
157
+ PolyValue::Float(f)
158
+ } else {
159
+ PolyValue::String(s.clone())
160
+ }
161
+ }
162
+ ScalarType::Bool => {
163
+ let trimmed = s.trim().to_ascii_lowercase();
164
+ if trimmed == "true" || trimmed == "1" {
165
+ PolyValue::Bool(true)
166
+ } else if trimmed == "false" || trimmed == "0" {
167
+ PolyValue::Bool(false)
168
+ } else {
169
+ PolyValue::String(s.clone())
170
+ }
171
+ }
172
+ _ => PolyValue::String(s.clone()),
173
+ },
174
+ other => PolyValue::from(other.clone()),
175
+ }
176
+ }
177
+
178
+ /// Convert a `serde_json::Value` into a `PolyValue` matching a `ModelSchema`.
179
+ pub fn json_value_to_poly_value(val: &JsonValue, schema: &ModelSchema) -> Result<PolyValue> {
180
+ match val {
181
+ JsonValue::Object(map) => {
182
+ let mut obj = HashMap::with_capacity(schema.fields.len());
183
+ for field in &schema.fields {
184
+ let xml_name_str = std::str::from_utf8(&field.xml_name).ok();
185
+ // Priority: match schema alias (xml_name) first, fallback to python field.name
186
+ let found_val = xml_name_str
187
+ .and_then(|alias| map.get(alias))
188
+ .or_else(|| map.get(&field.name));
189
+
190
+ if let Some(json_field_val) = found_val {
191
+ if json_field_val.is_null() {
192
+ obj.insert(field.name.clone(), PolyValue::Null);
193
+ continue;
194
+ }
195
+
196
+ match &field.val_type {
197
+ ValueType::Scalar(st) => {
198
+ obj.insert(field.name.clone(), coerce_scalar(json_field_val, st));
199
+ }
200
+ ValueType::List(inner) => {
201
+ if let JsonValue::Array(items) = json_field_val {
202
+ let mut poly_list = Vec::with_capacity(items.len());
203
+ for item in items {
204
+ if let ValueType::Nested(nested_schema) = inner.as_ref() {
205
+ poly_list
206
+ .push(json_value_to_poly_value(item, nested_schema)?);
207
+ } else if let ValueType::Scalar(st) = inner.as_ref() {
208
+ poly_list.push(coerce_scalar(item, st));
209
+ } else {
210
+ poly_list.push(PolyValue::from(item.clone()));
211
+ }
212
+ }
213
+ obj.insert(field.name.clone(), PolyValue::List(poly_list));
214
+ } else {
215
+ // Single item into list
216
+ let single = match inner.as_ref() {
217
+ ValueType::Nested(nested_schema) => {
218
+ json_value_to_poly_value(json_field_val, nested_schema)?
219
+ }
220
+ ValueType::Scalar(st) => coerce_scalar(json_field_val, st),
221
+ _ => PolyValue::from(json_field_val.clone()),
222
+ };
223
+ obj.insert(field.name.clone(), PolyValue::List(vec![single]));
224
+ }
225
+ }
226
+ ValueType::Nested(nested_schema) => {
227
+ obj.insert(
228
+ field.name.clone(),
229
+ json_value_to_poly_value(json_field_val, nested_schema)?,
230
+ );
231
+ }
232
+ }
233
+ }
234
+ }
235
+ Ok(PolyValue::Object(obj))
236
+ }
237
+ other => Ok(PolyValue::from(other.clone())),
238
+ }
239
+ }
240
+
241
+ /// Serialize a `PolyValue` into JSON bytes using a `ModelSchema`.
242
+ pub fn serialize_json(
243
+ value: &PolyValue,
244
+ schema: &ModelSchema,
245
+ indent: Option<usize>,
246
+ by_alias: bool,
247
+ ) -> Result<Vec<u8>> {
248
+ let json_val = poly_value_to_json_value(value, schema, by_alias);
249
+ match indent {
250
+ Some(spaces) if spaces > 0 => {
251
+ let indent_str = " ".repeat(spaces);
252
+ let formatter = serde_json::ser::PrettyFormatter::with_indent(indent_str.as_bytes());
253
+ let mut buf = Vec::new();
254
+ let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
255
+ json_val.serialize(&mut ser)?;
256
+ Ok(buf)
257
+ }
258
+ _ => Ok(serde_json::to_vec(&json_val)?),
259
+ }
260
+ }
261
+
262
+ /// Deserialize JSON bytes into a `PolyValue` using a `ModelSchema`.
263
+ pub fn deserialize_json(json_bytes: &[u8], schema: Arc<ModelSchema>) -> Result<PolyValue> {
264
+ let json_val: JsonValue = serde_json::from_slice(json_bytes)?;
265
+ json_value_to_poly_value(&json_val, &schema)
266
+ }
267
+
268
+ #[cfg(test)]
269
+ mod tests {
270
+ use super::*;
271
+ use crate::schema::{FieldKind, FieldSchema};
272
+
273
+ #[test]
274
+ fn test_json_roundtrip_primitives() {
275
+ let schema = ModelSchema::builder("User")
276
+ .field(FieldSchema::new(
277
+ "user_id",
278
+ b"userId",
279
+ FieldKind::Attribute,
280
+ ValueType::Scalar(ScalarType::Int),
281
+ ))
282
+ .field(FieldSchema::new(
283
+ "full_name",
284
+ b"fullName",
285
+ FieldKind::Element,
286
+ ValueType::Scalar(ScalarType::String),
287
+ ))
288
+ .field(FieldSchema::new(
289
+ "score",
290
+ b"score",
291
+ FieldKind::Element,
292
+ ValueType::Scalar(ScalarType::Float),
293
+ ))
294
+ .field(FieldSchema::new(
295
+ "is_admin",
296
+ b"isAdmin",
297
+ FieldKind::Element,
298
+ ValueType::Scalar(ScalarType::Bool),
299
+ ))
300
+ .build();
301
+
302
+ let mut map = HashMap::new();
303
+ map.insert("user_id".to_string(), PolyValue::Int(42));
304
+ map.insert(
305
+ "full_name".to_string(),
306
+ PolyValue::String("Ada Lovelace".to_string()),
307
+ );
308
+ map.insert("score".to_string(), PolyValue::Float(99.5));
309
+ map.insert("is_admin".to_string(), PolyValue::Bool(true));
310
+ let poly_val = PolyValue::Object(map);
311
+
312
+ // Serialize with by_alias = true
313
+ let json_bytes = serialize_json(&poly_val, &schema, None, true).unwrap();
314
+ let json_str = std::str::from_utf8(&json_bytes).unwrap();
315
+ assert!(json_str.contains("\"userId\":42"));
316
+ assert!(json_str.contains("\"fullName\":\"Ada Lovelace\""));
317
+ assert!(json_str.contains("\"score\":99.5"));
318
+ assert!(json_str.contains("\"isAdmin\":true"));
319
+
320
+ // Deserialize back
321
+ let parsed = deserialize_json(&json_bytes, schema).unwrap();
322
+ assert_eq!(parsed.get("user_id"), Some(&PolyValue::Int(42)));
323
+ assert_eq!(
324
+ parsed.get("full_name"),
325
+ Some(&PolyValue::String("Ada Lovelace".to_string()))
326
+ );
327
+ assert_eq!(parsed.get("score"), Some(&PolyValue::Float(99.5)));
328
+ assert_eq!(parsed.get("is_admin"), Some(&PolyValue::Bool(true)));
329
+ }
330
+
331
+ #[test]
332
+ fn test_json_deserialization_accepts_python_field_names() {
333
+ let schema = ModelSchema::builder("User")
334
+ .field(FieldSchema::new(
335
+ "user_id",
336
+ b"userId",
337
+ FieldKind::Attribute,
338
+ ValueType::Scalar(ScalarType::Int),
339
+ ))
340
+ .field(FieldSchema::new(
341
+ "full_name",
342
+ b"fullName",
343
+ FieldKind::Element,
344
+ ValueType::Scalar(ScalarType::String),
345
+ ))
346
+ .build();
347
+
348
+ let json = br#"{"user_id": 100, "full_name": "Grace Hopper"}"#;
349
+ let parsed = deserialize_json(json, schema).unwrap();
350
+ assert_eq!(parsed.get("user_id"), Some(&PolyValue::Int(100)));
351
+ assert_eq!(
352
+ parsed.get("full_name"),
353
+ Some(&PolyValue::String("Grace Hopper".to_string()))
354
+ );
355
+ }
356
+ }
@@ -2,6 +2,7 @@ pub mod codegen;
2
2
  pub mod converters;
3
3
  pub mod error;
4
4
  pub mod ir;
5
+ pub mod json;
5
6
  pub mod parser;
6
7
  pub mod schema;
7
8
  pub mod schema_parser;
@@ -9,6 +10,7 @@ pub mod serializer;
9
10
  pub mod value;
10
11
 
11
12
  pub use error::{PolyXmlError, Result};
13
+ pub use json::{deserialize_json, serialize_json};
12
14
  pub use parser::{XmlDeserializer, XmlItemStream};
13
15
  pub use schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
14
16
  pub use serializer::XmlSerializer;
@@ -64,4 +64,27 @@ impl PolyValue {
64
64
  _ => None,
65
65
  }
66
66
  }
67
+
68
+ /// Serialize this PolyValue into raw JSON bytes.
69
+ pub fn to_json(&self, indent: Option<usize>) -> crate::error::Result<Vec<u8>> {
70
+ let json_val = serde_json::Value::from(self);
71
+ match indent {
72
+ Some(spaces) if spaces > 0 => {
73
+ let indent_str = " ".repeat(spaces);
74
+ let formatter =
75
+ serde_json::ser::PrettyFormatter::with_indent(indent_str.as_bytes());
76
+ let mut buf = Vec::new();
77
+ let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
78
+ serde::Serialize::serialize(&json_val, &mut ser)?;
79
+ Ok(buf)
80
+ }
81
+ _ => Ok(serde_json::to_vec(&json_val)?),
82
+ }
83
+ }
84
+
85
+ /// Deserialize raw JSON bytes into a dynamic PolyValue without a schema.
86
+ pub fn from_json(json_bytes: &[u8]) -> crate::error::Result<Self> {
87
+ let json_val: serde_json::Value = serde_json::from_slice(json_bytes)?;
88
+ Ok(PolyValue::from(json_val))
89
+ }
67
90
  }