Table of Contents

Diagnostics

ShapeShift reports problems in two complementary ways.

  • At run time, every serialization failure carries a ShapeShiftPath breadcrumb that names the exact value that failed.
  • At build time, the ShapeShift analyzers move the most common authoring mistakes forward from the first serialization attempt to the compiler.

Exception paths

When a value deep inside an object graph cannot be written or read, the failure is attributed to the precise location in the document. Each converter that is responsible for an enclosing map property or vector element attaches its step to the exception as the failure unwinds, so the outermost frame observes a complete path from the root of the document.

var serializer = new JsonSerializer();

string json = """
    {
        "Id": 5,
        "Lines": [
            { "Sku": "a-1", "Quantity": 2 },
            { "Sku": "b-2", "Quantity": "two" }
        ]
    }
    """;

try
{
    Order? order = serializer.Deserialize<Order>(json);
}
catch (ShapeShiftSerializationException ex)
{
    // ex.Path is $.Lines[1].Quantity, so the failing value can be located
    // in the document without re-reading the whole payload.
    Console.WriteLine(ex.Path);

    // The message ends with "Path: $.Lines[1].Quantity." and the original
    // decoder failure is preserved as the inner exception.
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.InnerException?.Message);
}

Path is a ShapeShiftPath, so it can be compared, rendered in JSONPath-like notation, or handed straight back to TryDeserializeFragment to re-read just the offending fragment.

Breadcrumbs are attached for:

  • nested object properties, on both the serializing and deserializing side,
  • vector elements, by index,
  • string-keyed map entries, by property name,
  • non-string-keyed dictionary entries, as [entry][0] for the key and [entry][1] for the value,
  • rectangular array elements, as [1][flattenedIndex] within the dimensions/values envelope,
  • union payloads, as [1] within the discriminator/value envelope,
  • extension-data properties, by property name.

Exceptions thrown by your own code — including custom converters — are never swallowed. A non-ShapeShift exception is wrapped in a ShapeShiftSerializationException that preserves the original as its InnerException; a ShapeShift exception is re-thrown as-is with its stack trace intact and only its path extended. Cancellation propagates untouched.

Custom converters that iterate over sub-values can participate by calling AddEnclosingPathElement(ShapeShiftPathElement) from an exception filter:

internal sealed class TotalsConverter : ShapeShiftConverter<int[], JsonEncoder, JsonDecoder>
{
    public override int[]? Read(ref JsonDecoder decoder, SerializationContext<JsonEncoder, JsonDecoder> context)
        => throw new NotSupportedException();

    public override void Write(ref JsonEncoder encoder, in int[]? value, SerializationContext<JsonEncoder, JsonDecoder> context)
    {
        if (value is null)
        {
            encoder.WriteNull();
            return;
        }

        encoder.WriteStartVector(value.Length);
        for (int i = 0; i < value.Length; i++)
        {
            try
            {
                encoder.WriteValue(value[i]);
            }
            catch (ShapeShiftSerializationException ex) when (ex.AddEnclosingPathElement(i))
            {
                // AddEnclosingPathElement always returns true, so the filter falls through
                // to this rethrow, which preserves the original stack trace.
                throw;
            }
        }

        encoder.WriteEndVector();
    }
}

The method always returns true, which makes the filter fall through to the throw; that rethrows the original exception without disturbing its stack trace.