cedar_policy_core/entities/json/
err.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::fmt::Display;
18
19use super::SchemaType;
20use crate::ast::{
21    BorrowedRestrictedExpr, EntityAttrEvaluationError, EntityUID, Expr, ExprKind, PolicyID,
22    RestrictedExpr, RestrictedExpressionError, Type,
23};
24use crate::entities::conformance::err::EntitySchemaConformanceError;
25use crate::entities::{Name, ReservedNameError};
26use crate::parser::err::ParseErrors;
27use either::Either;
28use itertools::Itertools;
29use miette::Diagnostic;
30use smol_str::SmolStr;
31use thiserror::Error;
32
33/// Escape kind
34#[derive(Debug)]
35pub enum EscapeKind {
36    /// Escape `__entity`
37    Entity,
38    /// Escape `__extn`
39    Extension,
40}
41
42impl Display for EscapeKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Entity => write!(f, "__entity"),
46            Self::Extension => write!(f, "__extn"),
47        }
48    }
49}
50
51/// Errors thrown during deserialization from JSON
52//
53// CAUTION: this type is publicly exported in `cedar-policy`.
54// Don't make fields `pub`, don't make breaking changes, and use caution
55// when adding public methods.
56#[derive(Debug, Diagnostic, Error)]
57#[non_exhaustive]
58pub enum JsonDeserializationError {
59    /// Error thrown by the `serde_json` crate
60    #[error(transparent)]
61    #[diagnostic(transparent)]
62    Serde(#[from] JsonError),
63    /// Contents of an escape failed to parse.
64    #[error(transparent)]
65    #[diagnostic(transparent)]
66    ParseEscape(ParseEscape),
67    /// Restricted expression error
68    #[error(transparent)]
69    #[diagnostic(transparent)]
70    RestrictedExpressionError(#[from] RestrictedExpressionError),
71    /// A field that needs to be a literal entity reference, was some other JSON value
72    #[error(transparent)]
73    #[diagnostic(transparent)]
74    ExpectedLiteralEntityRef(ExpectedLiteralEntityRef),
75    /// A field that needs to be an extension value, was some other JSON value
76    #[error(transparent)]
77    #[diagnostic(transparent)]
78    ExpectedExtnValue(ExpectedExtnValue),
79    /// Parents of actions should be actions, but this action has a non-action parent
80    #[error(transparent)]
81    #[diagnostic(transparent)]
82    ActionParentIsNotAction(ActionParentIsNotAction),
83    /// Schema-based parsing needed an implicit extension constructor, but no suitable
84    /// constructor was found
85    #[error(transparent)]
86    #[diagnostic(transparent)]
87    MissingImpliedConstructor(MissingImpliedConstructor),
88    /// The same key appears two or more times in a single record
89    #[error(transparent)]
90    #[diagnostic(transparent)]
91    DuplicateKey(DuplicateKey),
92    /// Error when evaluating an entity attribute
93    #[error(transparent)]
94    #[diagnostic(transparent)]
95    EntityAttributeEvaluation(#[from] EntityAttrEvaluationError),
96    /// During schema-based parsing, encountered an entity which does not
97    /// conform to the schema.
98    ///
99    /// This error contains the `Entity` analogues some of the other errors
100    /// listed below, among other things.
101    #[error(transparent)]
102    #[diagnostic(transparent)]
103    EntitySchemaConformance(EntitySchemaConformanceError),
104    /// During schema-based parsing, encountered this attribute on a record, but
105    /// that attribute shouldn't exist on that record
106    #[error(transparent)]
107    #[diagnostic(transparent)]
108    UnexpectedRecordAttr(UnexpectedRecordAttr),
109    /// During schema-based parsing, didn't encounter this attribute of a
110    /// record, but that attribute should have existed
111    #[error(transparent)]
112    #[diagnostic(transparent)]
113    MissingRequiredRecordAttr(MissingRequiredRecordAttr),
114    /// During schema-based parsing, found a different type than the schema indicated.
115    ///
116    /// (This is used in all cases except inside entity attributes; type mismatches in
117    /// entity attributes are reported as `Self::EntitySchemaConformance`. As of
118    /// this writing, that means this should only be used for schema-based
119    /// parsing of the `Context`.)
120    #[error(transparent)]
121    #[diagnostic(transparent)]
122    TypeMismatch(TypeMismatch),
123    /// Raised when a JsonValue contains the no longer supported `__expr` escape
124    #[error("{0}, the `__expr` escape is no longer supported")]
125    #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
126    ExprTag(Box<JsonDeserializationErrorContext>),
127    /// Raised when the input JSON contains a `null`
128    #[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
129    Null(Box<JsonDeserializationErrorContext>),
130    /// Returned when a name contains `__cedar`
131    #[error(transparent)]
132    #[diagnostic(transparent)]
133    ReservedName(#[from] ReservedNameError),
134    /// Never returned as of 4.2.0 (entity tags are now stable), but this error
135    /// variant not removed because that would be a breaking change on this
136    /// publicly-exported type.
137    #[deprecated(
138        since = "4.2.0",
139        note = "entity-tags is now stable and fully supported, so this error never occurs"
140    )]
141    #[error("entity tags are not supported in this build; to use entity tags, you must enable the `entity-tags` experimental feature")]
142    UnsupportedEntityTags,
143}
144
145impl JsonDeserializationError {
146    pub(crate) fn parse_escape(
147        kind: EscapeKind,
148        value: impl Into<String>,
149        errs: ParseErrors,
150    ) -> Self {
151        Self::ParseEscape(ParseEscape {
152            kind,
153            value: value.into(),
154            errs,
155        })
156    }
157
158    pub(crate) fn expected_entity_ref(
159        ctx: JsonDeserializationErrorContext,
160        got: Either<serde_json::Value, Expr>,
161    ) -> Self {
162        Self::ExpectedLiteralEntityRef(ExpectedLiteralEntityRef {
163            ctx: Box::new(ctx),
164            got: Box::new(got),
165        })
166    }
167
168    pub(crate) fn expected_extn_value(
169        ctx: JsonDeserializationErrorContext,
170        got: Either<serde_json::Value, Expr>,
171    ) -> Self {
172        Self::ExpectedExtnValue(ExpectedExtnValue {
173            ctx: Box::new(ctx),
174            got: Box::new(got),
175        })
176    }
177
178    pub(crate) fn action_parent_is_not_action(uid: EntityUID, parent: EntityUID) -> Self {
179        Self::ActionParentIsNotAction(ActionParentIsNotAction { uid, parent })
180    }
181
182    pub(crate) fn missing_implied_constructor(
183        ctx: JsonDeserializationErrorContext,
184        return_type: SchemaType,
185    ) -> Self {
186        Self::MissingImpliedConstructor(MissingImpliedConstructor {
187            ctx: Box::new(ctx),
188            return_type: Box::new(return_type),
189        })
190    }
191
192    pub(crate) fn duplicate_key(
193        ctx: JsonDeserializationErrorContext,
194        key: impl Into<SmolStr>,
195    ) -> Self {
196        Self::DuplicateKey(DuplicateKey {
197            ctx: Box::new(ctx),
198            key: key.into(),
199        })
200    }
201
202    pub(crate) fn unexpected_record_attr(
203        ctx: JsonDeserializationErrorContext,
204        record_attr: impl Into<SmolStr>,
205    ) -> Self {
206        Self::UnexpectedRecordAttr(UnexpectedRecordAttr {
207            ctx: Box::new(ctx),
208            record_attr: record_attr.into(),
209        })
210    }
211
212    pub(crate) fn missing_required_record_attr(
213        ctx: JsonDeserializationErrorContext,
214        record_attr: impl Into<SmolStr>,
215    ) -> Self {
216        Self::MissingRequiredRecordAttr(MissingRequiredRecordAttr {
217            ctx: Box::new(ctx),
218            record_attr: record_attr.into(),
219        })
220    }
221
222    pub(crate) fn type_mismatch(
223        ctx: JsonDeserializationErrorContext,
224        err: TypeMismatchError,
225    ) -> Self {
226        Self::TypeMismatch(TypeMismatch {
227            ctx: Box::new(ctx),
228            err,
229        })
230    }
231}
232
233#[derive(Debug, Error, Diagnostic)]
234#[error("{ctx}, {err}")]
235/// General error for type mismatches
236pub struct TypeMismatch {
237    /// Context of this error, which will be something other than `EntityAttribute`.
238    /// (Type mismatches in entity attributes are reported as
239    /// `Self::EntitySchemaConformance`.)
240    ctx: Box<JsonDeserializationErrorContext>,
241    /// Underlying error
242    #[diagnostic(transparent)]
243    err: TypeMismatchError,
244}
245
246#[derive(Debug, Error, Diagnostic)]
247#[error("{}, expected the record to have an attribute `{}`, but it does not", .ctx, .record_attr)]
248/// Error type for a record missing a required attr
249pub struct MissingRequiredRecordAttr {
250    /// Context of this error
251    ctx: Box<JsonDeserializationErrorContext>,
252    /// Name of the (Record) attribute which was expected
253    record_attr: SmolStr,
254}
255
256#[derive(Debug, Diagnostic, Error)]
257#[error("{}, record attribute `{}` should not exist according to the schema", .ctx, .record_attr)]
258/// Error type for record attributes that should not exist
259pub struct UnexpectedRecordAttr {
260    /// Context of this error
261    ctx: Box<JsonDeserializationErrorContext>,
262    /// Name of the (Record) attribute which was unexpected
263    record_attr: SmolStr,
264}
265
266#[derive(Debug, Error, Diagnostic)]
267#[error("{}, duplicate key `{}` in record", .ctx, .key)]
268/// Error type for records having duplicate keys
269pub struct DuplicateKey {
270    /// Context of this error
271    ctx: Box<JsonDeserializationErrorContext>,
272    /// The key that appeared two or more times
273    key: SmolStr,
274}
275
276#[derive(Debug, Error, Diagnostic)]
277#[error("{}, missing extension constructor for {}", .ctx, .return_type)]
278#[diagnostic(help("expected a value of type {} because of the schema", .return_type))]
279/// Error type for missing extension constructors
280pub struct MissingImpliedConstructor {
281    /// Context of this error
282    ctx: Box<JsonDeserializationErrorContext>,
283    /// return type of the constructor we were looking for
284    return_type: Box<SchemaType>,
285}
286
287#[derive(Debug, Error, Diagnostic)]
288#[error("action `{}` has a non-action parent `{}`", .uid, .parent)]
289#[diagnostic(help("parents of actions need to have type `Action` themselves, perhaps namespaced"))]
290/// Error type for action  parents not having type `Action`
291pub struct ActionParentIsNotAction {
292    /// Action entity that had the invalid parent
293    uid: EntityUID,
294    /// Parent that is invalid
295    parent: EntityUID,
296}
297
298#[derive(Debug, Error, Diagnostic)]
299#[error("failed to parse escape `{kind}`: {value}, errors: {errs}")]
300#[diagnostic(help("{}", match .kind {
301        EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
302        EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
303    }))]
304/// Error type for incorrect escaping
305pub struct ParseEscape {
306    /// Escape kind
307    kind: EscapeKind,
308    /// Escape value at fault
309    value: String,
310    /// Parse errors
311    #[diagnostic(transparent)]
312    errs: ParseErrors,
313}
314
315#[derive(Debug, Error, Diagnostic)]
316#[error("{}, expected a literal entity reference, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
317#[diagnostic(help(
318    r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
319))]
320/// Error type for getting any expression other than an entity reference
321pub struct ExpectedLiteralEntityRef {
322    /// Context of this error
323    ctx: Box<JsonDeserializationErrorContext>,
324    /// the expression we got instead
325    got: Box<Either<serde_json::Value, Expr>>,
326}
327
328#[derive(Debug, Error, Diagnostic)]
329#[error("{}, expected an extension value, but got `{}`", .ctx, display_json_value(.got.as_ref()))]
330#[diagnostic(help(r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#))]
331/// Error type for getting any expression other than en extesion value
332pub struct ExpectedExtnValue {
333    /// Context of this error
334    ctx: Box<JsonDeserializationErrorContext>,
335    /// the expression we got instead
336    got: Box<Either<serde_json::Value, Expr>>,
337}
338
339#[derive(Debug, Error, Diagnostic)]
340#[error(transparent)]
341/// Wrapper type for errors from `serde_json`
342pub struct JsonError(#[from] serde_json::Error);
343
344impl From<serde_json::Error> for JsonDeserializationError {
345    fn from(value: serde_json::Error) -> Self {
346        Self::Serde(JsonError(value))
347    }
348}
349
350impl From<serde_json::Error> for JsonSerializationError {
351    fn from(value: serde_json::Error) -> Self {
352        Self::Serde(JsonError(value))
353    }
354}
355
356/// Errors thrown during serialization to JSON
357#[derive(Debug, Diagnostic, Error)]
358#[non_exhaustive]
359pub enum JsonSerializationError {
360    /// Error thrown by `serde_json`
361    #[error(transparent)]
362    #[diagnostic(transparent)]
363    Serde(#[from] JsonError),
364    /// Extension-function calls with 0 arguments are not currently supported in
365    /// our JSON format.
366    #[error(transparent)]
367    #[diagnostic(transparent)]
368    ExtnCall0Arguments(ExtnCall0Arguments),
369    /// Extension-function calls with 2 or more arguments are not currently
370    /// supported in our JSON format.
371    #[error(transparent)]
372    #[diagnostic(transparent)]
373    ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments),
374    /// Encountered a `Record` which can't be serialized to JSON because it
375    /// contains a key which is reserved as a JSON escape.
376    #[error(transparent)]
377    #[diagnostic(transparent)]
378    ReservedKey(ReservedKey),
379    /// Encountered an `ExprKind` which we didn't expect. Either a case is
380    /// missing in `CedarValueJson::from_expr()`, or an internal invariant was
381    /// violated and there is a non-restricted expression in `RestrictedExpr`
382    #[error(transparent)]
383    #[diagnostic(transparent)]
384    UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind),
385    /// Encountered a (partial-evaluation) residual which can't be encoded in
386    /// JSON
387    #[error(transparent)]
388    #[diagnostic(transparent)]
389    Residual(Residual),
390}
391
392impl JsonSerializationError {
393    pub(crate) fn call_0_args(func: Name) -> Self {
394        Self::ExtnCall0Arguments(ExtnCall0Arguments { func })
395    }
396
397    pub(crate) fn call_2_or_more_args(func: Name) -> Self {
398        Self::ExtnCall2OrMoreArguments(ExtnCall2OrMoreArguments { func })
399    }
400
401    pub(crate) fn reserved_key(key: impl Into<SmolStr>) -> Self {
402        Self::ReservedKey(ReservedKey { key: key.into() })
403    }
404
405    pub(crate) fn unexpected_restricted_expr_kind(kind: ExprKind) -> Self {
406        Self::UnexpectedRestrictedExprKind(UnexpectedRestrictedExprKind { kind })
407    }
408
409    pub(crate) fn residual(residual: Expr) -> Self {
410        Self::Residual(Residual { residual })
411    }
412}
413
414/// Error type for extension functions called w/ 0 arguments
415#[derive(Debug, Error, Diagnostic)]
416#[error("unsupported call to `{}` with 0 arguments", .func)]
417#[diagnostic(help(
418    "extension function calls with 0 arguments are not currently supported in our JSON format"
419))]
420pub struct ExtnCall0Arguments {
421    /// Name of the function which was called with 0 arguments
422    func: Name,
423}
424
425/// Error type for extension functions called w/ 2+ arguments
426#[derive(Debug, Error, Diagnostic)]
427#[error("unsupported call to `{}` with 2 or more arguments", .func)]
428#[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
429pub struct ExtnCall2OrMoreArguments {
430    /// Name of the function called w/ 2 or more arguments
431    func: Name,
432}
433
434/// Error type for using a reserved key in a record
435#[derive(Debug, Error, Diagnostic)]
436#[error("record uses reserved key `{}`", .key)]
437pub struct ReservedKey {
438    /// The reserved key used
439    key: SmolStr,
440}
441
442impl ReservedKey {
443    /// The reserved keyword used as a key
444    pub fn key(&self) -> impl AsRef<str> + '_ {
445        &self.key
446    }
447}
448
449/// Error type for a restricted expression containing a non-restricted expression
450#[derive(Debug, Error, Diagnostic)]
451#[error("unexpected restricted expression `{:?}`", .kind)]
452pub struct UnexpectedRestrictedExprKind {
453    /// The [`ExprKind`] we didn't expend to find
454    kind: ExprKind,
455}
456
457/// Error type for residuals that can't be serialized
458#[derive(Debug, Error, Diagnostic)]
459#[error("cannot encode residual as JSON: {}", .residual)]
460pub struct Residual {
461    /// The residual that can't be serialized
462    residual: Expr,
463}
464
465/// Gives information about the context of a JSON deserialization error (e.g.,
466/// where we were in the JSON document).
467#[derive(Debug, Clone)]
468pub enum JsonDeserializationErrorContext {
469    /// The error occurred while deserializing the attribute `attr` of an entity.
470    EntityAttribute {
471        /// Entity where the error occurred
472        uid: EntityUID,
473        /// Attribute where the error occurred
474        attr: SmolStr,
475    },
476    /// The error occurred while deserializing the tag `tag` of an entity.
477    EntityTag {
478        /// Entity where the error occurred
479        uid: EntityUID,
480        /// Tag where the error occurred
481        tag: SmolStr,
482    },
483    /// The error occurred while deserializing the `parents` field of an entity.
484    EntityParents {
485        /// Entity where the error occurred
486        uid: EntityUID,
487    },
488    /// The error occurred while deserializing the `uid` field of an entity.
489    EntityUid,
490    /// The error occurred while deserializing the `Context`.
491    Context,
492    /// The error occurred while deserializing a policy in JSON (EST) form.
493    Policy {
494        /// ID of the policy we were deserializing
495        id: PolicyID,
496    },
497    /// The error occured while deserializing a template link
498    TemplateLink,
499    /// The context was unknown, this shouldn't surface to users
500    Unknown,
501}
502
503/// Type mismatch error (in terms of `SchemaType`)
504#[derive(Debug, Diagnostic, Error)]
505#[error("type mismatch: value was expected to have type {expected}, but it {mismatch_reason}: `{}`",
506    display_restricted_expr(.actual_val.as_borrowed()),
507)]
508pub struct TypeMismatchError {
509    /// Type which was expected
510    expected: Box<SchemaType>,
511    /// Reason for the type mismatch
512    mismatch_reason: TypeMismatchReason,
513    /// Value which doesn't have the expected type
514    actual_val: Box<RestrictedExpr>,
515}
516
517#[derive(Debug, Error)]
518enum TypeMismatchReason {
519    /// We saw this dynamic type which is not compatible with the expected
520    /// schema type.
521    #[error("actually has type {0}")]
522    UnexpectedType(Type),
523    /// We saw a record type expression as expected, but it contains an
524    /// attribute we didn't expect.
525    #[error("contains an unexpected attribute `{0}`")]
526    UnexpectedAttr(SmolStr),
527    /// We saw a record type expression as expected, but it did not contain an
528    /// attribute we expected.
529    #[error("is missing the required attribute `{0}`")]
530    MissingRequiredAtr(SmolStr),
531    /// No further detail available.
532    #[error("does not")]
533    None,
534}
535
536impl TypeMismatchError {
537    pub(crate) fn type_mismatch(
538        expected: SchemaType,
539        actual_ty: Option<Type>,
540        actual_val: RestrictedExpr,
541    ) -> Self {
542        Self {
543            expected: Box::new(expected),
544            mismatch_reason: match actual_ty {
545                Some(ty) => TypeMismatchReason::UnexpectedType(ty),
546                None => TypeMismatchReason::None,
547            },
548            actual_val: Box::new(actual_val),
549        }
550    }
551
552    pub(crate) fn unexpected_attr(
553        expected: SchemaType,
554        unexpected_attr: SmolStr,
555        actual_val: RestrictedExpr,
556    ) -> Self {
557        Self {
558            expected: Box::new(expected),
559            mismatch_reason: TypeMismatchReason::UnexpectedAttr(unexpected_attr),
560            actual_val: Box::new(actual_val),
561        }
562    }
563
564    pub(crate) fn missing_required_attr(
565        expected: SchemaType,
566        missing_attr: SmolStr,
567        actual_val: RestrictedExpr,
568    ) -> Self {
569        Self {
570            expected: Box::new(expected),
571            mismatch_reason: TypeMismatchReason::MissingRequiredAtr(missing_attr),
572            actual_val: Box::new(actual_val),
573        }
574    }
575}
576
577impl std::fmt::Display for JsonDeserializationErrorContext {
578    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
579        match self {
580            Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
581            Self::EntityTag { uid, tag } => write!(f, "in tag `{tag}` on `{uid}`"),
582            Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
583            Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
584            Self::Context => write!(f, "while parsing context"),
585            Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
586            Self::TemplateLink => write!(f, "while parsing a template link"),
587            Self::Unknown => write!(f, "parsing context was unknown, please file a bug report at https://github.com/cedar-policy/cedar so we can improve this error message"),
588        }
589    }
590}
591
592fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
593    match v {
594        Either::Left(json) => display_value(json),
595        Either::Right(e) => e.to_string(),
596    }
597}
598
599/// Display a `serde_json::Value`, but sorting object attributes, so that the
600/// output is deterministic (important for tests that check equality of error
601/// messages).
602///
603/// Note that this doesn't sort array elements, because JSON arrays are ordered,
604/// so all JSON-handling functions naturally preserve order for arrays and thus
605/// provide a deterministic output.
606fn display_value(v: &serde_json::Value) -> String {
607    match v {
608        serde_json::Value::Array(contents) => {
609            format!("[{}]", contents.iter().map(display_value).join(", "))
610        }
611        serde_json::Value::Object(map) => {
612            let mut v: Vec<_> = map.iter().collect();
613            // We sort the keys here so that our error messages are consistent and defined
614            v.sort_by_key(|p| p.0);
615            let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
616            format!("{{{}}}", v.iter().map(display_kv).join(","))
617        }
618        other => other.to_string(),
619    }
620}
621
622/// Display a `RestrictedExpr`, but sorting record attributes and set elements,
623/// so that the output is deterministic (important for tests that check equality
624/// of error messages).
625fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
626    match expr.expr_kind() {
627        ExprKind::Set(elements) => {
628            let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); // since the RestrictedExpr invariant holds for the input, it holds for all set elements
629            format!(
630                "[{}]",
631                restricted_exprs
632                    .map(display_restricted_expr)
633                    .sorted_unstable()
634                    .join(", ")
635            )
636        }
637        ExprKind::Record(m) => {
638            format!(
639                "{{{}}}",
640                m.iter()
641                    .sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
642                    .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
643                    .join(", ")
644            )
645        }
646        _ => format!("{expr}"), // all other cases: use the normal Display
647    }
648}