diff --git a/Source/Serilog.Exceptions/Formatting/StructuredExceptionFormatter.cs b/Source/Serilog.Exceptions/Formatting/StructuredExceptionFormatter.cs
new file mode 100644
index 00000000..eebd44b3
--- /dev/null
+++ b/Source/Serilog.Exceptions/Formatting/StructuredExceptionFormatter.cs
@@ -0,0 +1,101 @@
+namespace Serilog.Exceptions.Formatting;
+
+using System;
+using System.IO;
+using Serilog.Events;
+using Serilog.Exceptions.Core;
+using Serilog.Formatting;
+using Serilog.Formatting.Json;
+
+///
+/// A JSON text formatter using structured properties for exceptions.
+///
+///
+/// Avoids the redundancy of when used with .
+///
+public class StructuredExceptionFormatter : ITextFormatter
+{
+ private readonly string rootName;
+ private readonly JsonValueFormatter valueFormatter;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The root name used by the enricher, if different from the default.
+ /// A custom JSON formatter to use for underlying properties, if any.
+ public StructuredExceptionFormatter(string? rootName = null, JsonValueFormatter? valueFormatter = null)
+ {
+ this.rootName = rootName ?? new DestructuringOptionsBuilder().RootName;
+ this.valueFormatter = valueFormatter ?? new();
+ }
+
+ ///
+ public void Format(LogEvent logEvent, TextWriter output)
+ {
+#if NET6_0_OR_GREATER
+ ArgumentNullException.ThrowIfNull(logEvent);
+ ArgumentNullException.ThrowIfNull(output);
+#else
+ if (logEvent is null)
+ {
+ throw new ArgumentNullException(nameof(logEvent));
+ }
+
+ if (output is null)
+ {
+ throw new ArgumentNullException(nameof(output));
+ }
+#endif
+
+ output.Write("{\"Timestamp\":\"");
+ output.Write(logEvent.Timestamp.UtcDateTime.ToString("O"));
+
+ output.Write("\",\"Message\":");
+ var message = logEvent.MessageTemplate.Render(logEvent.Properties);
+ JsonValueFormatter.WriteQuotedJsonString(message, output);
+
+ output.Write(",\"Level\":\"");
+ output.Write(logEvent.Level);
+ output.Write('\"');
+
+ var propCount = logEvent.Properties.Count;
+
+ if (logEvent.Properties.TryGetValue(this.rootName, out var exceptionProperty))
+ {
+ output.Write(",\"Exception\":");
+ this.valueFormatter.Format(exceptionProperty, output);
+ propCount--;
+ }
+
+ if (propCount > 0)
+ {
+ output.Write(",\"Properties\":{");
+ var comma = false;
+
+ foreach (var property in logEvent.Properties)
+ {
+ if (property.Key == this.rootName)
+ {
+ continue;
+ }
+
+ if (comma)
+ {
+ output.Write(',');
+ }
+ else
+ {
+ comma = true;
+ }
+
+ JsonValueFormatter.WriteQuotedJsonString(property.Key, output);
+ output.Write(':');
+ this.valueFormatter.Format(property.Value, output);
+ }
+
+ output.Write("}");
+ }
+
+ output.WriteLine('}');
+ }
+}
diff --git a/Tests/Serilog.Exceptions.Test/Formatting/StructuredExceptionFormatterTest.cs b/Tests/Serilog.Exceptions.Test/Formatting/StructuredExceptionFormatterTest.cs
new file mode 100644
index 00000000..b565a196
--- /dev/null
+++ b/Tests/Serilog.Exceptions.Test/Formatting/StructuredExceptionFormatterTest.cs
@@ -0,0 +1,53 @@
+namespace Serilog.Exceptions.Test.Formatting;
+
+using System;
+using System.IO;
+using FluentAssertions.Execution;
+using Serilog.Events;
+using Serilog.Exceptions.Formatting;
+using Serilog.Parsing;
+using Xunit;
+
+public class StructuredExceptionFormatterTest
+{
+ [Fact]
+ public void Format_EventNoProperties_CorrectJson() =>
+ Format_CorrectJson(
+ "{\"Timestamp\":\"2021-12-01T13:15:00.0000000Z\",\"Message\":\"Hello!\",\"Level\":\"Debug\"}",
+ new DateTimeOffset(2021, 12, 1, 12, 15, 0, 0, TimeSpan.FromHours(-1)),
+ LogEventLevel.Debug,
+ "Hello!");
+
+ [Fact]
+ public void Format_EventWithProperties_CorrectJson() =>
+ Format_CorrectJson(
+ "{\"Timestamp\":\"1999-01-01T04:15:12.0550000Z\",\"Message\":\"Hello, \\\"Kathy\\\"!\",\"Level\":\"Information\",\"Properties\":{\"Person\":\"Kathy\",\"Extra\":[\"more\",\"data\"]}}",
+ new DateTimeOffset(1999, 1, 1, 4, 15, 12, 55, TimeSpan.Zero),
+ LogEventLevel.Information,
+ "Hello, {Person}!",
+ new("Person", new ScalarValue("Kathy")),
+ new("Extra", new SequenceValue(new ScalarValue[] { new("more"), new("data") })));
+
+ [Fact]
+ public void Format_EventWithException_CorrectJson() =>
+ Format_CorrectJson(
+ "{\"Timestamp\":\"2001-01-01T22:30:12.0000000Z\",\"Message\":\"Uh, oh!\",\"Level\":\"Error\",\"Exception\":{\"Message\":\"Bad stuff\",\"HResult\":1234}}",
+ new DateTimeOffset(2001, 1, 2, 1, 30, 12, 0, TimeSpan.FromHours(3)),
+ LogEventLevel.Error,
+ "Uh, oh!",
+ new LogEventProperty("ExceptionDetail", new StructureValue(new LogEventProperty[] { new("Message", new ScalarValue("Bad stuff")), new("HResult", new ScalarValue(1234)) })));
+
+ private static void Format_CorrectJson(
+ string expected,
+ DateTimeOffset timestamp,
+ LogEventLevel level,
+ string template,
+ params LogEventProperty[] properties)
+ {
+ using var output = new StringWriter();
+ var ev = new LogEvent(timestamp, level, null, new MessageTemplateParser().Parse(template), properties);
+
+ new StructuredExceptionFormatter().Format(ev, output);
+ Assert.Equal(expected + Environment.NewLine, output.ToString());
+ }
+}