use serde_json::Value;
#[cfg(any(feature = "decode-protobuf", feature = "validate-json"))]
use super::compiled::CompiledCache;
use super::validate::{NotValidated, Verdict};
use super::{SchemaKind, TypeSchema, WireEncoding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodedPayload {
pub value: Value,
pub notes: Vec<String>,
pub verdict: Verdict,
}
#[derive(Debug, thiserror::Error)]
pub enum DecodeError {
#[error("no decoder for schema kind {0:?} — render structurally instead")]
UnknownKind(String),
#[error("payload does not decode as {codec}: {message}")]
Malformed {
codec: &'static str,
message: String,
#[source]
cause: Option<BoxedCause>,
},
#[error("encoding {0:?} is not decodable under this schema kind")]
WrongEncoding(String),
#[error("schema entry is incomplete: {message}")]
BadSchema {
message: String,
#[source]
cause: Option<BoxedCause>,
},
#[error("value does not conform for encoding: {message}")]
Encode {
message: String,
#[source]
cause: Option<BoxedCause>,
},
}
pub type BoxedCause = Box<dyn std::error::Error + Send + Sync>;
impl DecodeError {
pub fn malformed(codec: &'static str, cause: impl Into<BoxedCause>) -> Self {
let cause = cause.into();
DecodeError::Malformed {
codec,
message: cause.to_string(),
cause: Some(cause),
}
}
pub fn malformed_here(codec: &'static str, message: impl Into<String>) -> Self {
DecodeError::Malformed {
codec,
message: message.into(),
cause: None,
}
}
pub fn bad_schema(message: impl Into<String>) -> Self {
DecodeError::BadSchema {
message: message.into(),
cause: None,
}
}
pub fn bad_schema_from(cause: impl Into<BoxedCause>) -> Self {
let cause = cause.into();
DecodeError::BadSchema {
message: cause.to_string(),
cause: Some(cause),
}
}
pub fn encode(cause: impl Into<BoxedCause>) -> Self {
let cause = cause.into();
DecodeError::Encode {
message: cause.to_string(),
cause: Some(cause),
}
}
pub fn encode_here(message: impl Into<String>) -> Self {
DecodeError::Encode {
message: message.into(),
cause: None,
}
}
}
pub trait PayloadDecoder: Send + Sync {
fn kind(&self) -> &str;
fn decode(
&self,
schema: &TypeSchema,
encoding: &WireEncoding,
bytes: &[u8],
) -> Result<DecodedPayload, DecodeError>;
fn encode(
&self,
schema: &TypeSchema,
value: &Value,
target: &WireEncoding,
) -> Result<Vec<u8>, DecodeError>;
}
#[derive(Default)]
pub struct JsonSchemaDecoder {
#[cfg(feature = "validate-json")]
validators: CompiledCache<jsonschema::Validator>,
}
impl JsonSchemaDecoder {
pub fn new() -> JsonSchemaDecoder {
JsonSchemaDecoder::default()
}
#[cfg(feature = "validate-json")]
fn verdict(&self, schema: &TypeSchema, value: &Value) -> Verdict {
let compiled = self.validators.get_or_compile(schema, |schema| {
let doc = schema
.json_document()
.ok_or_else(|| DecodeError::bad_schema("missing json document"))?;
jsonschema::validator_for(doc).map_err(DecodeError::bad_schema_from)
});
match compiled {
Ok(validator) => super::validate::validate_json(&validator, value),
Err(_) => Verdict::NotValidated(NotValidated::BadSchema),
}
}
#[cfg(not(feature = "validate-json"))]
fn verdict(&self, _schema: &TypeSchema, _value: &Value) -> Verdict {
Verdict::NotValidated(NotValidated::FeatureOff)
}
fn undeclared_fields(schema: &TypeSchema, value: &Value) -> Vec<String> {
let Some(doc) = schema.json_document() else {
return Vec::new();
};
let Some(props) = doc.get("properties").and_then(Value::as_object) else {
return Vec::new();
};
let Some(obj) = value.as_object() else {
return Vec::new();
};
obj.keys()
.filter(|k| !props.contains_key(*k))
.map(|k| format!("field {k:?} is not in the served schema (additive evolution?)"))
.collect()
}
}
impl PayloadDecoder for JsonSchemaDecoder {
fn kind(&self) -> &str {
SchemaKind::JSON_SCHEMA
}
fn decode(
&self,
schema: &TypeSchema,
encoding: &WireEncoding,
bytes: &[u8],
) -> Result<DecodedPayload, DecodeError> {
let value: Value = match encoding {
WireEncoding::Json => {
serde_json::from_slice(bytes).map_err(|e| DecodeError::malformed("json", e))?
}
WireEncoding::Cbor => {
let cbor: ciborium::Value =
ciborium::from_reader(bytes).map_err(|e| DecodeError::malformed("cbor", e))?;
serde_json::to_value(&cbor).map_err(|e| DecodeError::malformed("cbor->json", e))?
}
other => return Err(DecodeError::WrongEncoding(format!("{other:?}"))),
};
let notes = Self::undeclared_fields(schema, &value);
let verdict = self.verdict(schema, &value);
Ok(DecodedPayload {
value,
notes,
verdict,
})
}
fn encode(
&self,
schema: &TypeSchema,
value: &Value,
target: &WireEncoding,
) -> Result<Vec<u8>, DecodeError> {
if let Verdict::Invalid(errors) = self.verdict(schema, value) {
return Err(DecodeError::encode_here(format!(
"value violates the served schema: {}",
errors.join("; ")
)));
}
match target {
WireEncoding::Json => serde_json::to_vec(value).map_err(DecodeError::encode),
WireEncoding::Cbor => {
let mut out = Vec::new();
ciborium::into_writer(value, &mut out).map_err(DecodeError::encode)?;
Ok(out)
}
other => Err(DecodeError::WrongEncoding(format!("{other:?}"))),
}
}
}
#[cfg(feature = "decode-protobuf")]
#[derive(Default)]
pub struct ProtobufDecoder {
descriptors: CompiledCache<prost_reflect::MessageDescriptor>,
}
#[cfg(feature = "decode-protobuf")]
impl ProtobufDecoder {
pub fn new() -> ProtobufDecoder {
ProtobufDecoder::default()
}
pub fn compilations(&self) -> u64 {
self.descriptors.compilations()
}
fn descriptor(
&self,
schema: &TypeSchema,
) -> Result<std::sync::Arc<prost_reflect::MessageDescriptor>, DecodeError> {
self.descriptors.get_or_compile(schema, |schema| {
let fds = schema
.protobuf_descriptor_set()
.ok_or_else(|| DecodeError::bad_schema("missing descriptor_b64"))?;
let message = schema
.protobuf_message()
.ok_or_else(|| DecodeError::bad_schema("missing message name"))?;
let pool = prost_reflect::DescriptorPool::decode(fds.as_slice())
.map_err(|e| DecodeError::bad_schema(format!("descriptor set: {e}")))?;
pool.get_message_by_name(message)
.ok_or_else(|| DecodeError::bad_schema(format!("message {message:?} not in set")))
})
}
}
#[cfg(feature = "decode-protobuf")]
impl PayloadDecoder for ProtobufDecoder {
fn kind(&self) -> &str {
SchemaKind::PROTOBUF
}
fn decode(
&self,
schema: &TypeSchema,
encoding: &WireEncoding,
bytes: &[u8],
) -> Result<DecodedPayload, DecodeError> {
match encoding {
WireEncoding::Protobuf | WireEncoding::Other(_) => {}
WireEncoding::Json | WireEncoding::Cbor | WireEncoding::Cdr => {
return Err(DecodeError::WrongEncoding(format!("{encoding:?}")));
}
}
let desc = self.descriptor(schema)?;
let msg = prost_reflect::DynamicMessage::decode((*desc).clone(), bytes)
.map_err(|e| DecodeError::malformed("protobuf", e))?;
let value =
serde_json::to_value(&msg).map_err(|e| DecodeError::malformed("protobuf->json", e))?;
Ok(DecodedPayload {
value,
notes: Vec::new(),
verdict: Verdict::Valid,
})
}
fn encode(
&self,
schema: &TypeSchema,
value: &Value,
_target: &WireEncoding,
) -> Result<Vec<u8>, DecodeError> {
use prost::Message as _;
let desc = self.descriptor(schema)?;
let rendered = serde_json::to_string(value).map_err(DecodeError::encode)?;
let mut deserializer = serde_json::Deserializer::from_str(&rendered);
let msg = prost_reflect::DynamicMessage::deserialize((*desc).clone(), &mut deserializer)
.map_err(DecodeError::encode)?;
deserializer.end().map_err(DecodeError::encode)?;
Ok(msg.encode_to_vec())
}
}
pub struct DecoderRegistry {
decoders: Vec<Box<dyn PayloadDecoder>>,
}
impl Default for DecoderRegistry {
fn default() -> Self {
Self::new()
}
}
impl DecoderRegistry {
pub fn new() -> Self {
#[allow(unused_mut)]
let mut decoders: Vec<Box<dyn PayloadDecoder>> = vec![Box::new(JsonSchemaDecoder::new())];
#[cfg(feature = "decode-protobuf")]
decoders.push(Box::new(ProtobufDecoder::new()));
#[cfg(feature = "decode-cdr")]
decoders.push(Box::new(super::cdr::CdrDecoder::new()));
DecoderRegistry { decoders }
}
pub fn register(&mut self, decoder: Box<dyn PayloadDecoder>) {
self.decoders.insert(0, decoder);
}
fn find(&self, kind: &str) -> Option<&dyn PayloadDecoder> {
self.decoders
.iter()
.find(|d| d.kind() == kind)
.map(Box::as_ref)
}
pub fn decode(
&self,
schema: &TypeSchema,
encoding: &WireEncoding,
bytes: &[u8],
) -> Result<DecodedPayload, DecodeError> {
self.find(schema.kind_str())
.ok_or_else(|| DecodeError::UnknownKind(schema.kind_str().to_string()))?
.decode(schema, encoding, bytes)
}
pub fn encode(
&self,
schema: &TypeSchema,
value: &Value,
target: &WireEncoding,
) -> Result<Vec<u8>, DecodeError> {
self.find(schema.kind_str())
.ok_or_else(|| DecodeError::UnknownKind(schema.kind_str().to_string()))?
.encode(schema, value, target)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn point_schema() -> TypeSchema {
TypeSchema::json_schema(json!({
"type": "object",
"properties": { "x": { "type": "integer" }, "y": { "type": "integer" } },
}))
}
#[test]
fn json_and_cbor_framings_decode_to_the_same_value() {
let registry = DecoderRegistry::new();
let schema = point_schema();
let value = json!({"x": 1, "y": 2});
let json_bytes = serde_json::to_vec(&value).unwrap();
let mut cbor_bytes = Vec::new();
ciborium::into_writer(&value, &mut cbor_bytes).unwrap();
let a = registry
.decode(&schema, &WireEncoding::Json, &json_bytes)
.unwrap();
let b = registry
.decode(&schema, &WireEncoding::Cbor, &cbor_bytes)
.unwrap();
assert_eq!(a.value, value);
assert_eq!(b.value, value);
assert!(a.notes.is_empty());
}
#[test]
fn undeclared_fields_are_noted_not_fatal() {
let registry = DecoderRegistry::new();
let schema = point_schema();
let bytes = serde_json::to_vec(&json!({"x": 1, "z": 9})).unwrap();
let out = registry
.decode(&schema, &WireEncoding::Json, &bytes)
.unwrap();
assert_eq!(out.notes.len(), 1);
assert!(out.notes[0].contains("\"z\""));
}
#[test]
fn encode_round_trips_both_framings() {
let registry = DecoderRegistry::new();
let schema = point_schema();
let value = json!({"x": 7, "y": 8});
for enc in [WireEncoding::Json, WireEncoding::Cbor] {
let bytes = registry.encode(&schema, &value, &enc).unwrap();
let back = registry.decode(&schema, &enc, &bytes).unwrap();
assert_eq!(back.value, value, "{enc:?}");
}
}
#[test]
fn unknown_kind_is_an_honest_error() {
let registry = DecoderRegistry::new();
let json = r#"{
"schema_version": 1, "app": "t",
"types": { "W": { "kind": "cddl", "hash": "sha256:00", "spec": "x = int" } }
}"#;
let set = crate::schema::SchemaSet::parse(json).unwrap();
let err = registry
.decode(set.get("W").unwrap(), &WireEncoding::Json, b"{}")
.unwrap_err();
assert!(matches!(err, DecodeError::UnknownKind(k) if k == "cddl"));
}
#[cfg(feature = "decode-protobuf")]
fn tiny_fds() -> Vec<u8> {
fn ld(tag: u8, bytes: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.push(u8::try_from(bytes.len()).unwrap());
out.extend_from_slice(bytes);
out
}
fn varint_field(tag: u8, v: u8) -> Vec<u8> {
vec![tag, v]
}
let field_x = {
let mut f = ld(0x0a, b"x"); f.extend(varint_field(0x18, 1)); f.extend(varint_field(0x20, 1)); f.extend(varint_field(0x28, 5)); f.extend(ld(0x52, b"x")); f
};
let field_name = {
let mut f = ld(0x0a, b"name");
f.extend(varint_field(0x18, 2));
f.extend(varint_field(0x20, 1));
f.extend(varint_field(0x28, 9)); f.extend(ld(0x52, b"name"));
f
};
let msg = {
let mut m = ld(0x0a, b"Blob");
m.extend(ld(0x12, &field_x));
m.extend(ld(0x12, &field_name));
m
};
let file = {
let mut f = ld(0x0a, b"t.proto");
f.extend(ld(0x12, b"t"));
f.extend(ld(0x22, &msg));
f
};
ld(0x0a, &file)
}
#[cfg(feature = "decode-protobuf")]
#[test]
fn protobuf_dynamic_decode_and_encode() {
let registry = DecoderRegistry::new();
let schema = TypeSchema::protobuf("t.Blob", &tiny_fds());
let value = json!({"x": 42, "name": "hi"});
let bytes = registry
.encode(&schema, &value, &WireEncoding::Protobuf)
.unwrap();
let out = registry
.decode(&schema, &WireEncoding::Protobuf, &bytes)
.unwrap();
assert_eq!(out.value.get("x"), Some(&json!(42)));
assert_eq!(out.value.get("name"), Some(&json!("hi")));
}
}
#[cfg(all(test, feature = "validate-json"))]
mod validate_tests {
use super::*;
use serde_json::json;
fn strict_schema() -> TypeSchema {
TypeSchema::json_schema(json!({
"type": "object",
"required": ["x"],
"properties": { "x": { "type": "integer" } },
}))
}
#[test]
fn decode_verdicts_follow_the_schema() {
let registry = DecoderRegistry::new();
let s = strict_schema();
let ok = serde_json::to_vec(&json!({"x": 1})).unwrap();
let out = registry.decode(&s, &WireEncoding::Json, &ok).unwrap();
assert_eq!(out.verdict, Verdict::Valid);
let bad = serde_json::to_vec(&json!({"x": "seven"})).unwrap();
let out = registry.decode(&s, &WireEncoding::Json, &bad).unwrap();
match out.verdict {
Verdict::Invalid(errors) => {
assert_eq!(errors.len(), 1, "{errors:?}");
assert!(errors[0].contains("/x"), "{errors:?}");
}
other => panic!("expected Invalid, got {other:?}"),
}
}
#[test]
fn encode_refuses_a_nonconformant_value() {
let registry = DecoderRegistry::new();
let s = strict_schema();
let err = registry
.encode(&s, &json!({"x": "seven"}), &WireEncoding::Json)
.unwrap_err();
assert!(matches!(err, DecodeError::Encode { .. }), "{err:?}");
assert!(err.to_string().contains("violates"), "{err}");
assert!(
registry
.encode(&s, &json!({"x": 1}), &WireEncoding::Json)
.is_ok()
);
}
#[test]
fn the_validator_compiles_once_across_many_samples() {
let codec = JsonSchemaDecoder::new();
let s = strict_schema();
let bytes = serde_json::to_vec(&json!({"x": 1})).unwrap();
for _ in 0..20 {
codec.decode(&s, &WireEncoding::Json, &bytes).unwrap();
}
assert_eq!(codec.validators.compilations(), 1);
}
}
#[cfg(all(test, feature = "decode-protobuf"))]
mod protobuf_compiled_tests {
use super::*;
fn descriptor_set(extra_field: bool) -> Vec<u8> {
use prost::Message as _;
use prost_reflect::prost_types::{
DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
field_descriptor_proto,
};
let field = |name: &str, number: i32| FieldDescriptorProto {
name: Some(name.to_string()),
number: Some(number),
label: Some(field_descriptor_proto::Label::Optional as i32),
r#type: Some(field_descriptor_proto::Type::Double as i32),
json_name: Some(name.to_string()),
..Default::default()
};
let mut fields = vec![field("v", 1)];
if extra_field {
fields.push(field("w", 2));
}
FileDescriptorSet {
file: vec![FileDescriptorProto {
name: Some("t.proto".into()),
package: Some("t".into()),
message_type: vec![DescriptorProto {
name: Some("M".into()),
field: fields,
..Default::default()
}],
..Default::default()
}],
}
.encode_to_vec()
}
fn schema(extra_field: bool) -> TypeSchema {
TypeSchema::protobuf("t.M", &descriptor_set(extra_field))
}
#[test]
fn the_descriptor_pool_is_built_once_across_many_samples() {
let codec = ProtobufDecoder::new();
let s = schema(false);
let value = serde_json::json!({"v": 12.5});
let bytes = codec
.encode(&s, &value, &WireEncoding::Protobuf)
.expect("encodes");
for _ in 0..50 {
let out = codec
.decode(&s, &WireEncoding::Protobuf, &bytes)
.expect("decodes");
assert_eq!(out.value, value);
}
assert_eq!(
codec.compilations(),
1,
"the descriptor set must be parsed once, not per sample"
);
}
#[test]
fn a_changed_schema_hash_rebuilds_the_pool() {
let codec = ProtobufDecoder::new();
let (a, b) = (schema(false), schema(true));
assert_ne!(a.hash(), b.hash(), "the fixture must actually differ");
let av = serde_json::json!({"v": 1.0});
let bv = serde_json::json!({"v": 1.0, "w": 2.0});
let ab = codec.encode(&a, &av, &WireEncoding::Protobuf).unwrap();
let bb = codec.encode(&b, &bv, &WireEncoding::Protobuf).unwrap();
assert_eq!(codec.compilations(), 2);
assert_eq!(
codec
.decode(&b, &WireEncoding::Protobuf, &bb)
.unwrap()
.value,
bv,
"the new schema decodes its own new field"
);
assert_eq!(
codec
.decode(&a, &WireEncoding::Protobuf, &ab)
.unwrap()
.value,
av
);
assert_eq!(codec.compilations(), 2, "both pools were already built");
}
}