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
Outstanding work on the spike/source-generator branch before it is ready to merge to master and ship as v2. For historical bug/fix context see memory-bank/progress.md and git log.
Last updated: 2026-04-23.
Scope update (2026-04-23). All three Tier-2 features —
[ExpandFrom],[Translator]with DI,IPopcornBlindHandler<TFrom,TTo>— cleared from scope after use-case analysis showed each had a cleaner answer using patterns already native to ASP.NET Core + System.Text.Json. Each drop is documented with a recommended replacement in docs/MigrationV7toV8.md (§5, §7, §8). v2.0 is now feature-complete; the two remaining merge-gate items are both infrastructure (AOT CI + NuGet packaging).
UsePopcornExceptionHandler + [SubPropertyDefault]: shipped.Popcorn.FunctionalTests (2 remaining skips are the polymorphism dispatch feature — see Tier 2 section below). 19 passing in Popcorn.SourceGenerator.Tests. Zero CS86xx warnings in generated code.PopcornAotExample builds with PublishAot=True and exercises a custom [PopcornEnvelope] shape.PopcornNetStandard*): still in the tree, unchanged. Planned removal after v2 ships side-by-side for a release or two.All three planned Tier-2 features were considered and dropped after use-case analysis. The consistent finding: what v7 shipped as dedicated framework surface is, in v8, better served by patterns already native to ASP.NET Core + System.Text.Json. Each drop is documented in the migration guide with the recommended replacement:
[ExpandFrom] — use [Never] on internal source properties, a 3-line hand-written factory, or Mapster.SourceGenerator for complex mapping. See MigrationV7toV8.md §7.[Translator] with DI — resolve at the endpoint (batchable, clear I/O boundaries, testable); computed properties still work for pure transforms. Serializing with injected services is an antipattern (N+1 queries, hidden I/O, scope threading complexity). See MigrationV7toV8.md §5.IPopcornBlindHandler<TFrom,TTo> — standard JsonConverter<T> registered on JsonSerializerOptions.Converters covers the full use case and composes with Popcorn transparently. See MigrationV7toV8.md §8.If a real consumer presents a concrete case that none of the replacement patterns cover, the specs live on in the git history and can be revived — but spec-driven shipping of features nobody has asked for is the shape of complexity we are deliberately shedding.
PolymorphismTests.cs.
PolymorphicCollection_EmitsDiscriminator_WhenConfigured: abstract/interface base + registered derived types via [JsonDerivedType]; generator needs to emit per-item type-dispatch.ExpanderGenerator.cs:PolymorphicUnknownDescriptor). Generator now emits a warning when a member is typed object, abstract class, or interface. 5 tests in EnvelopeDiagnosticsTests.cs cover the positive and negative cases. Registered-derived support (the [JsonDerivedType] dispatch half) remains unimplemented.[Factory]-tagged static method for instantiating types during read.Dictionary<string, object> contexts — dropped (superseded by DI).benchmarks/results/v2-baseline/. Covers Stj (reflection) vs Stj (source-gen) vs Popcorn (source-gen) vs legacy PopcornNetStandard (reflection) across SimpleModel, SimpleModelList[100], ComplexNestedModel, ComplexNestedModelList[25]. Three incremental generator optimizations landed after initial baseline capture — walk-through under opt-iterations/.All, ~5.8× for Default on ComplexModelList). Popcorn-default on ComplexModelList is ~10× faster / ~5× less alloc than STJ reflection. Popcorn-all on ComplexModelList is 0.87× time / 0.93× alloc — Popcorn is faster than STJ when emitting everything on nested data; legacy-all is 3.6× slower than STJ on the same shape..github/workflows/benchmarks.yml runs on PR + push to master / spike/**. dotnet run -- ci in SerializationPerformance runs 5 filtered benchmarks (SimpleJob — 3 warmup + 15 iterations, ~2 min wall time), then .github/scripts/compare-benchmark-ratios.py computes three load-bearing ratios (Popcorn / STJ-source-gen on the same shape) and compares against benchmarks/results/ci-baseline.json. Fails if any ratio regressed by more than thresholdPercent (default 25%). Improvements > 25% print an informational note that reminds you to update the baseline.SimpleModelList_PopcornAll_vs_Stj (worst-case 1.49), ComplexModelList_PopcornAll_vs_Stj (headline 0.87 — Popcorn is faster than STJ), ComplexModelList_PopcornDefault_vs_Stj (selectivity 0.11).ci locally and commit the new baseline in the same PR. When an intentional regression (new feature that trades perf for ergonomics) ships, bump the baseline upward.$GITHUB_STEP_SUMMARY; raw BDN artifacts uploaded for 30 days..github/workflows/tests.yml runs on PR + push to master / spike/**. Installs the .NET 10 SDK pinned by global.json, caches NuGet packages keyed on csproj hashes, runs dotnet test on both Popcorn.FunctionalTests (net8.0 + net10.0, 182 passing / 2 skipped each; the 8.0.x SDK is installed for its runtime) and Popcorn.SourceGenerator.Tests (19 passing). trx logs uploaded as an artifact on failure. Concurrency-group cancels superseded runs.aot-ci.yml: the AOT workflow needs the AOT toolchain (clang/zlib) + Docker; the test workflow should run faster and with fewer dependencies. Parallel jobs keep PR feedback tight.spike/source-generator without catching. Closed..github/workflows/aot-ci.yml runs on PR + push to master / spike/**. Uses docker/build-push-action@v5 with type=gha cache to build dotnet/PopcornAotExample/Dockerfile (context: dotnet/). Starts the container on port 8080, waits up to 60s for readiness, verifies all four endpoints end-to-end:
/todos — Success:true, Id:1 + Id:2 present, IsComplete ([Never]) absent./null — Data:null in the envelope./sub — Id:1 + nested ToDo object./boom — status 500, Ok:false, Problem populated with the exception message (exercises the exception middleware + generator-emitted custom-envelope error writer).docker logs. Always: stops the container. Concurrency-group cancels superseded runs on the same ref.Skyward.Api.Popcorn.SourceGen.Shared (runtime attributes, envelopes, middleware — from Popcorn.Shared.csproj) and Skyward.Api.Popcorn.SourceGen (analyzer-only, from Popcorn.SourceGenerator.csproj). Side-by-side-installable with legacy Skyward.Api.Popcorn v7 because the IDs diverge.PackageId, Version=8.0.0-preview.1, Authors, Description, PackageTags, PackageProjectUrl, RepositoryUrl, PackageLicenseFile=LICENSE, PackageReadmeFile=README.md, Copyright. Both reference Microsoft.SourceLink.GitHub with PublishRepositoryUrl=true and EmbedUntrackedSources=true. SourceGen is marked DevelopmentDependency=true + SuppressDependenciesWhenPacking=true so it flows analyzer-only and declares no runtime dependencies. SourceGen.Shared has IncludeSymbols=true + SymbolPackageFormat=snupkg.SourceGen embeds Popcorn.Shared.dll into analyzers/dotnet/cs/ (required for Roslyn to resolve attribute symbols during generation). The separate SourceGen.Shared package provides the runtime-visible copy under lib/netstandard2.0/. Consumers install both; they serve different layers..github/workflows/main.yml packs both v8 packages on release/v* tags. fetch-depth: 0 so SourceLink can resolve commit hashes. Two jobs: pack (uploads the nuget-packages artifact; does not use the NuGet key) and publish (the only job using the org secret SKYWARDNUGETAPIKEY; runs under the nuget-release environment, so a required reviewer approves each NuGet push and GitHub release; suffixed versions become GitHub prereleases). The workflow does not bump csproj <Version>; that happens by PR before tagging (checklist in docs/Releases.md).dotnet pack produces Skyward.Api.Popcorn.SourceGen.Shared.8.0.0-preview.1.nupkg (16 KB, lib/netstandard2.0/Popcorn.Shared.dll + deps) and Skyward.Api.Popcorn.SourceGen.8.0.0-preview.1.nupkg (38 KB, analyzers/dotnet/cs/ containing both dlls, no lib/, no transitive deps). Snupkg generated for Shared.net9.0 classlib with <PackageReference> to both packages from a local feed: packages restored, analyzer ran, generated SmokeConsumerCarJsonConverter.g.cs + SystemCollectionsGenericListSmokeConsumerCarJsonConverter.g.cs + RegisterConverters.g.cs, STJ source generator picked up the emitted Pop<Car> / Pop<List<Car>> types (visible in SmokeJsonContext.PopCar.g.cs / SmokeJsonContext.PopListCar.g.cs), build clean (0 errors, only informational JSG002 logs).docs/Releases.md with the preview entry.8.0.0-preview.1, push tag, CI pushes to NuGet. Shipped 2026-04-24 (tag release/v8.0.0-preview.1).8.0.0-preview.1 is on NuGet.SetContext(dict), custom envelope + middleware for SetInspector(lambda), include-parameter wire-name contract, JSG008 documentation, rollback plan.<PackageReleaseNotes> banner pointing at MigrationV7toV8.md.”Releases.md (currently empty of v8 entries) with an 8.0.0-preview.1 entry when it cuts.dotnet/Examples/PopcornNet5Example/ still references the v7 reflection engine (services.UsePopcorn((config) => config.UseDefaultConfiguration()), ExpandServiceFilter) and targets net5.0. Either port to v8 (minimal API + IPopcornAccessor + [JsonSerializable] context) or delete and rely solely on dotnet/PopcornAotExample/ as the canonical example.Confirmed against ExpanderGenerator.cs on 2026-04-23.
ExpanderGenerator.cs:908 emits #pragma warning disable CS8619, CS8600, CS8601, CS8625 at file scope. CS8619 / CS8625 are load-bearing (NRT-cast through generated code). CS8600 / CS8601 are pulled in defensively; could theoretically mask a real null bug introduced by a future generator change. Narrow to per-statement where feasible.class SettingsDict : Dictionary<string, string> {} has TypeArguments.Length == 0; ExpanderGenerator.cs:847 accesses namedDictionaryTypeNonNullable.TypeArguments[1] unguarded → IndexOutOfRangeException. No test hits it today. Fix: walk the IDictionary<K, V> interface chain for TypeArguments rather than reading the target type’s own list.IsBlindSerializableType uses stringly-typed hashset lookups. Matches the pre-existing convention (NumberTypes, StringTypes, BoolTypes, IgnoreTypes all compared via ToDisplayString().Replace("?", "")). Fragile to Roslyn display-format changes but consistent. Future cleanup: replace all such lookups with SpecialType / ITypeSymbol identity comparisons.IsNamedTypeCycleSafe (ExpanderGenerator.cs:133-164) treats any type NOT in allTypeNames as cycle-safe. Correct today — unregistered user types fall through to JsonSerializer.Serialize which doesn’t touch Popcorn’s HashSet. Revisit if a future change starts recursing through such types (e.g. IPopcornBlindHandler landing).Three generator-level optimizations considered but not taken in the 2026-04 opt pass. Listed in rough order of expected payoff.
JsonEncodedText. STJ’s own source-gen path does this; saves per-property UTF-16→UTF-8 encoding cost. Complicated by runtime PropertyNamingPolicy (the encoded form depends on options → forces per-options caching). Biggest remaining lever; would likely close most of the remaining SimpleModelList_PopcornAll 1.40× gap vs raw STJ.useAll && !hasNegations. Every property unconditionally emits under !all with no negations, so the scan is pure overhead. A one-time check at the top of the body could bypass the per-property loop entirely. Moderate payoff on Popcorn_All scenarios.POPCORN-INCLUDE)GET /foo?include=[very,long,list,...] can blow past proxy limits. An alternative header carries the same grammar, parsed by PopcornAccessor with header-first / query-fallback priority.PopcornAccessor.PropertyReferences getter checks HttpContext.Request.Headers["POPCORN-INCLUDE"] first, falls back to query ?include=.memory-bank/apiDesign.md, not started. Decision: implement in this spike, or defer to v2.1?A defensible order that minimizes dependency chains and maximizes incremental merge-readiness:
Adjust based on what any real consumer blocks on first.
PopcornNetStandard) committed under benchmarks/results/v2-baseline/.Pop{X}Inner regression on nested-collection registrations..github/workflows/aot-ci.yml.Popcorn.SourceGenerator + Popcorn.Shared — two-package design Skyward.Api.Popcorn.SourceGen + Skyward.Api.Popcorn.SourceGen.Shared, 8.0.0-preview.1, shipped to NuGet 2026-04-24.