Skip to content

REST JSON

Two Smithy protocols share the same JSON-over-HTTP wire format with REST bindings:

ProtocolTraitStatus
alloy#simpleRestJson@simpleRestJsonPreview — client + server
aws.protocols#restJson1@restJson1Preview — client + server

Current conformance snapshot from the pinned protocol-test models:

  • alloy#simpleRestJson: 43/43 official request/response cases (100%)
  • aws.protocols#restJson1: 268/272 official request/response cases (98.53%)

The modeling syntax, core HTTP binding traits, and generated C# shapes are nearly identical between the two protocols for common cases. The differences are in protocol-specific behavior: restJson1 adds AWS-specific features such as raw string/blob payloads, @requestCompression, @httpChecksumRequired, and a different error deserialization convention.

Use aws.protocols#restJson1 for new services. It has broad cross-ecosystem compatibility — most official Smithy code generators (Java, TypeScript, Python, Swift, Rust, Go) target restJson1 — and NSmithy generates OpenAPI from it, giving you Scalar UI and standard tooling out of the box.

Use alloy#simpleRestJson if your consumers are exclusively .NET or Scala (via Smithy4s) and you don’t need OpenAPI or cross-language reach. It has 100% conformance coverage but is narrower in ecosystem compatibility.

alloy#simpleRestJson requires alloy-core:

"com.disneystreaming.alloy:alloy-core:0.3.38"

aws.protocols#restJson1 requires smithy-aws-traits instead:

"software.amazon.smithy:smithy-aws-traits:1.56.0"
PurposePackage
ClientNSmithy.Client
Server (ASP.NET Core)NSmithy.Server.AspNetCore + Microsoft.AspNetCore.App

The example below is adapted from the Smithy quickstart. It demonstrates resources, pagination, errors, and common HTTP binding traits.

$version: "2"
namespace example.weather
use alloy#simpleRestJson
/// Provides weather forecasts.
@simpleRestJson
@paginated(inputToken: "nextToken", outputToken: "nextToken", pageSize: "pageSize")
service Weather {
version: "2006-03-01"
resources: [City]
operations: [GetCurrentTime]
}
resource City {
identifiers: { cityId: CityId }
properties: { coordinates: CityCoordinates }
read: GetCity
list: ListCities
resources: [Forecast]
}
resource Forecast {
identifiers: { cityId: CityId }
properties: { chanceOfRain: Float }
read: GetForecast
}
@pattern("^[A-Za-z0-9 ]+$")
string CityId
@readonly
@http(method: "GET", uri: "/current-time")
operation GetCurrentTime {
output := {
@required
time: Timestamp
}
}
@readonly
@http(method: "GET", uri: "/cities/{cityId}")
operation GetCity {
input := for City {
@required
@httpLabel
$cityId
}
output := for City {
@required
@notProperty
name: String
@required
$coordinates
}
errors: [NoSuchResource]
}
@readonly
@paginated(items: "items")
@http(method: "GET", uri: "/cities")
operation ListCities {
input := {
@httpQuery("nextToken")
nextToken: String
@httpQuery("pageSize")
pageSize: Integer
}
output := {
nextToken: String
@required
items: CitySummaries
}
}
@readonly
@http(method: "GET", uri: "/cities/{cityId}/forecast")
operation GetForecast {
input := for Forecast {
@required
@httpLabel
$cityId
}
output := for Forecast {
$chanceOfRain
}
}
structure CityCoordinates {
@required
latitude: Float
@required
longitude: Float
}
list CitySummaries {
member: CitySummary
}
@references([{resource: City}])
structure CitySummary {
@required
cityId: CityId
@required
name: String
}
@error("client")
structure NoSuchResource {
@required
resourceType: String
}

Key HTTP binding traits:

TraitBinds member to
@httpLabelURI path segment
@httpQuery("key")query string parameter
@httpHeader("name")request or response header
@httpPayloadraw request/response body

Members without an explicit binding go into the JSON body.

NSmithy generates one IWeatherServiceHandler interface with a method for each operation. Implement it once; the generated ASP.NET Core adapter handles routing, serialization, and error dispatch.

using Example.Weather;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddWeatherServiceHandler<WeatherHandler>();
var app = builder.Build();
app.MapWeatherServiceHttp();
app.Run();
internal sealed class WeatherHandler : IWeatherServiceHandler
{
public Task<GetCurrentTimeOutput> GetCurrentTimeAsync(
GetCurrentTimeInput input, CancellationToken ct = default) =>
Task.FromResult(new GetCurrentTimeOutput(DateTimeOffset.UtcNow));
public Task<GetCityOutput> GetCityAsync(
GetCityInput input, CancellationToken ct = default)
{
if (input.CityId == "unknown")
throw new NoSuchResource(null, "City");
return Task.FromResult(new GetCityOutput(
name: "Seattle",
coordinates: new CityCoordinates(47.6f, -122.3f)
));
}
public Task<ListCitiesOutput> ListCitiesAsync(
ListCitiesInput input, CancellationToken ct = default) =>
Task.FromResult(new ListCitiesOutput(new CitySummaries([
new CitySummary("SEA", "Seattle"),
new CitySummary("NYC", "New York"),
])));
public Task<GetForecastOutput> GetForecastAsync(
GetForecastInput input, CancellationToken ct = default) =>
Task.FromResult(new GetForecastOutput(chanceOfRain: 0.4f));
}

Throwing a generated error type from a handler method causes the adapter to serialize it with the correct HTTP status code and JSON body.

using Example.Weather;
using NSmithy.Client;
var client = new WeatherClient(
new HttpClient(),
new SmithyClientOptions { Endpoint = new Uri("http://localhost:5000") }
);
var time = await client.GetCurrentTimeAsync(new GetCurrentTimeInput());
Console.WriteLine(time.Time);
var cities = await client.ListCitiesAsync(new ListCitiesInput(pageSize: 10));
foreach (var c in cities.Items.Values)
Console.WriteLine($"{c.CityId}: {c.Name}");
var seattle = await client.GetCityAsync(new GetCityInput("SEA"));
Console.WriteLine($"{seattle.Name} ({seattle.Coordinates.Latitude}, {seattle.Coordinates.Longitude})");
var forecast = await client.GetForecastAsync(new GetForecastInput("SEA"));
Console.WriteLine($"Chance of rain: {forecast.ChanceOfRain:P0}");