Partial-response JSON for ASP.NET Core: API clients ask for exactly the fields they need. v8 is a Roslyn source generator for System.Text.Json and Native AOT.
This project is maintained by Skyward App Company
Drop Popcorn v8 into a minimal-API app in five minutes. For a full walkthrough (models, test data, multiple endpoints) see Getting Started.
v7 vs v8. This is the v8 (source-generator) path — it works under
PublishAot=TrueandPublishTrimmed=True. If you’re on the v7Skyward.Api.Popcorn/.DotNetCorepackages and aren’t ready to migrate, the v7 quick start is in the migration guide.
<PackageReference Include="Skyward.Api.Popcorn.SourceGen.Shared" Version="8.0.0-preview.1" />
<PackageReference Include="Skyward.Api.Popcorn.SourceGen" Version="8.0.0-preview.1" PrivateAssets="all" />
SourceGen is marked developmentDependency — it only contributes the Roslyn analyzer, never a
runtime DLL. SourceGen.Shared is the runtime library that carries attributes, envelopes, and
middleware.
Build requirement. .NET SDK 9.0.100 or later is required to build with the source generator (Roslyn 4.12); .NET 10 SDK recommended. In Visual Studio that means 2022 17.12 or later. The requirement is on the SDK that compiles your project, not on its target framework: a
net8.0app builds fine with the .NET 10 SDK. On an older SDK (the .NET 8 SDKs ship Roslyn 4.8–4.11) the compiler skips the generator with warningCS9057(“The analyzer assembly … references version ‘4.12.0.0’ of the compiler, which is newer than the currently running version …”), and the build then fails with errors about missing generated members such asAddPopcornOptions. Check withdotnet --version, or pin the SDK with aglobal.json.
JsonSerializerContextPopcorn’s generator discovers types through standard System.Text.Json
[JsonSerializable] attributes. Register each top-level response type — the generator walks
nested types automatically.
using System.Text.Json.Serialization;
using Popcorn.Shared;
[JsonSerializable(typeof(ApiResponse<List<Car>>))]
[JsonSerializable(typeof(ApiResponse<Car>))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
Program.csusing System.Text.Json;
using Popcorn.Shared;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddHttpContextAccessor();
builder.Services.AddPopcorn();
builder.Services.ConfigureHttpJsonOptions(o =>
{
o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
o.SerializerOptions.AddPopcornOptions(); // installs generator-emitted converters
});
var app = builder.Build();
app.UsePopcornExceptionHandler(); // unhandled exceptions → ApiError envelope
app.MapGet("/cars", (IPopcornAccessor access) =>
access.CreateResponse(new List<Car>
{
new(1, "Pontiac", "Firebird", 1981),
new(2, "Porsche", "Cayman", 2005),
}));
app.Run();
public record Car(int Id, string Make, string Model, int Year);
CreateSlimBuilder is the AOT-friendly host builder; if you don’t plan to publish as AOT you
can use WebApplication.CreateBuilder(args) instead.
GET /cars
→ { "Success": true, "Data": [{ "Id":1, "Make":"Pontiac", "Model":"Firebird", "Year":1981 }, ...] }
GET /cars?include=[Make,Model]
→ { "Success": true, "Data": [{ "Make":"Pontiac", "Model":"Firebird" }, ...] }
?include=
grammar (negation, wildcards, nesting).[Default] / [Always] /
[SubPropertyDefault] attribute usage.System.Text.Json and v7.dotnet/PopcornAotExample/ for a reference project that
publishes with PublishAot=True + PublishTrimmed=True.