diff --git a/README.md b/README.md
index 3cf7162f..f48c39be 100644
--- a/README.md
+++ b/README.md
@@ -128,28 +128,37 @@ var allDevices = await pages.FlattenToListAsync();
To resume pagination later, store `pagination.NextPageCursor` and pass it to
`NextPageAsync` on a new pager with the same request parameters.
-### Errors
+### Error Handling
Seam API errors raise a typed exception carrying the Seam error code, HTTP
-status code, and the `seam-request-id` to include in support requests:
+status code, and the `seam-request-id` to include in support requests.
+
+#### Validation errors
+
+When the API rejects a request because a parameter is invalid, it throws a
+`SeamHttpInvalidInputException`. Look up messages for a parameter you are
+already rendering, for example a field in a form:
```csharp
try
{
- await seam.Devices.GetAsync(new() { DeviceId = deviceId });
+ await seam.Devices.ListAsync(new() { DeviceIds = ["not-a-uuid"] });
}
catch (SeamHttpInvalidInputException exception)
{
- foreach (var message in exception.GetValidationErrorMessages("device_id"))
+ foreach (var message in exception.GetValidationErrorMessages("device_ids"))
Console.WriteLine(message);
}
-catch (SeamHttpUnauthorizedException)
-{
- // Invalid or expired credentials.
-}
-catch (SeamHttpApiException exception)
+```
+
+Or read every parameter that failed validation to summarize the request:
+
+```csharp
+foreach (var validationError in exception.ValidationErrors)
{
- Console.WriteLine($"{exception.Code} ({exception.RequestId})");
+ Console.WriteLine(
+ $"{validationError.ParameterName}: {string.Join(", ", validationError.ErrorMessages)}"
+ );
}
```
diff --git a/src/Seam/Exceptions/SeamHttpInvalidInputException.cs b/src/Seam/Exceptions/SeamHttpInvalidInputException.cs
index db8cbf74..a00b3ee7 100644
--- a/src/Seam/Exceptions/SeamHttpInvalidInputException.cs
+++ b/src/Seam/Exceptions/SeamHttpInvalidInputException.cs
@@ -4,6 +4,14 @@
namespace Seam
{
+ ///
+ /// A request parameter that failed validation and its error messages.
+ ///
+ public sealed record SeamValidationError(
+ string ParameterName,
+ IReadOnlyList ErrorMessages
+ );
+
///
/// Raised when the Seam API rejects the request parameters.
///
@@ -23,6 +31,34 @@ public SeamHttpInvalidInputException(
_validationErrors = validationErrors;
}
+ ///
+ /// Validation errors, one entry per failed request parameter.
+ ///
+ public IReadOnlyList ValidationErrors
+ {
+ get
+ {
+ if (_validationErrors is not { ValueKind: JsonValueKind.Object } validationErrors)
+ return Array.Empty();
+
+ var errors = new List();
+ foreach (var parameter in validationErrors.EnumerateObject())
+ {
+ if (parameter.Name != "_errors")
+ {
+ errors.Add(
+ new SeamValidationError(
+ parameter.Name,
+ GetValidationErrorMessages(parameter.Name)
+ )
+ );
+ }
+ }
+
+ return errors;
+ }
+ }
+
///
/// The validation messages for a request parameter, or an empty list when that parameter
/// has none.
diff --git a/test/Seam.Test/HttpErrorTests.cs b/test/Seam.Test/HttpErrorTests.cs
index 493cab4d..95d58d93 100644
--- a/test/Seam.Test/HttpErrorTests.cs
+++ b/test/Seam.Test/HttpErrorTests.cs
@@ -64,6 +64,7 @@ public class InvalidInputTests
"type": "invalid_input",
"message": "Invalid input",
"validation_errors": {
+ "_errors": ["Request is invalid"],
"device_ids": { "_errors": ["Expected array, received number"] }
},
"request_id": "request1"
@@ -104,6 +105,9 @@ public async Task ThrowsInvalidInputExceptionWithValidationMessages()
new[] { "Expected array, received number" },
exception.GetValidationErrorMessages("device_ids")
);
+ var validationError = Assert.Single(exception.ValidationErrors);
+ Assert.Equal("device_ids", validationError.ParameterName);
+ Assert.Equal(new[] { "Expected array, received number" }, validationError.ErrorMessages);
}
[Fact]