Skip to content

.Net: Fix RedisJsonCollection upsert persisting unannotated POCO properties#14030

Open
jluocsa wants to merge 3 commits into
microsoft:mainfrom
jluocsa:fix/redis-json-mapper-strip-unannotated
Open

.Net: Fix RedisJsonCollection upsert persisting unannotated POCO properties#14030
jluocsa wants to merge 3 commits into
microsoft:mainfrom
jluocsa:fix/redis-json-mapper-strip-unannotated

Conversation

@jluocsa
Copy link
Copy Markdown
Member

@jluocsa jluocsa commented May 23, 2026

Motivation and Context

Fixes #14021.

RedisJsonCollection<TKey, TRecord>.UpsertAsync was persisting POCO properties that are not part of the vector-store schema (no [VectorStoreKey] / [VectorStoreData] / [VectorStoreVector] attribute and not declared in the collection definition).

Root cause: RedisJsonMapper<TConsumerDataModel>.MapFromDataToStorageModel serialized the entire user record with JsonSerializer.SerializeToNode(dataModel, jsonSerializerOptions) and only removed the key field afterwards. Anything else on the type — including unannotated public properties — flowed straight into the Redis JSON payload.

RedisJsonDynamicMapper (the Dictionary<string, object?> path) already does the right thing: it iterates model.DataProperties / model.VectorProperties and only emits those.

Description

Refactor RedisJsonMapper<TConsumerDataModel>.MapFromDataToStorageModel to build the storage JsonObject explicitly from the CollectionModel, mirroring the dynamic mapper:

  1. Pull the key via model.KeyProperty.GetValueAsObject(dataModel) and validate it is a non-null string (unchanged semantics — Redis JSON keys are still string-typed and the constructor still validates that up front).
  2. Emit one JSON property per model.DataProperties entry, keyed by dataProperty.StorageName (which CollectionJsonModelBuilder.Customize() already populates from [JsonPropertyName] / the configured PropertyNamingPolicy / the .NET property name — exactly what the previous whole-object serialization produced for annotated properties). Each value is serialized through JsonSerializer.SerializeToNode(value, dataProperty.Type, jsonSerializerOptions) so all of the user's JsonSerializerOptions continue to apply.
  3. Emit one JSON property per model.VectorProperties entry, keyed by property.StorageName, with the existing precedence: generated embedding for this slot, otherwise the value pulled from the POCO. Vector values continue to be written as a flat JsonArray of float/double. The same ReadOnlyMemory<T> | Embedding<T> | T[] switch as in RedisJsonDynamicMapper is used, so both float and double vector types and their nullable variants are handled.

Properties without a vector-store attribute and not declared in the collection definition are now simply not serialized — same behavior as the dynamic mapper.

Tests:

  • RedisJsonCollectionTests.CanUpsertRecordAsync / CanUpsertManyRecordsAsync: dropped the "NotAnnotated":null / "notAnnotated":null fragments from the expected JSON payloads and removed the matching // TODO: Fix issue where NotAnnotated is being included in the JSON. comments.
  • RedisJsonMapperTests.MapsAllFieldsFromDataToStorageModel{,WithCustomSerializerOptions}: added a positive Assert.False(jsonObject.ContainsKey("NotAnnotated")) (and the notAnnotated camelCase variant) to lock in the fix.

Other tests in Redis.UnitTests were not touched.

Verification

$env:GITHUB_ACTIONS = 'true'   # skip the local pre-build `dotnet format` target
& $dotnet test .\dotnet\test\VectorData\Redis.UnitTests\Redis.UnitTests.csproj -c Release --verbosity normal

Result: Total tests: 109. Passed: 109. Failed: 0. Skipped: 0.

The targeted reproducer from the issue body:

& $dotnet test .\dotnet\test\VectorData\Redis.UnitTests\Redis.UnitTests.csproj `
    --filter "FullyQualifiedName~RedisJsonCollectionTests.CanUpsertRecordAsync" --verbosity normal

passes on all four [InlineData] rows.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • I ran all the unit tests in the affected project (Redis.UnitTests)
  • The PR follows the SK Contribution Guidelines and the pre-submission formatting script raises no violations
  • All unit tests pass, and I have added new tests where possible
  • I didn't break anyone 😄

Closes #14021

…erties (microsoft#14021)

RedisJsonMapper.MapFromDataToStorageModel previously serialized the entire record via JsonSerializer.SerializeToNode and only stripped the key, so any public POCO property without a [VectorStoreKey]/[VectorStoreData]/[VectorStoreVector] attribute (and not declared in the collection definition) was persisted to Redis.

Build the JSON payload explicitly from model.DataProperties and model.VectorProperties, mirroring RedisJsonDynamicMapper. Storage keys still come from property.StorageName (already populated by CollectionJsonModelBuilder from JsonPropertyName / PropertyNamingPolicy / the .NET property name), so the produced JSON for schema-annotated properties matches the previous behavior.

Updates RedisJsonCollectionTests upsert expectations (drops NotAnnotated:null fragments + TODOs) and adds an explicit assertion in RedisJsonMapperTests that unannotated properties are absent from the serialized JSON.
@jluocsa jluocsa requested a review from a team as a code owner May 23, 2026 18:59
Copilot AI review requested due to automatic review settings May 23, 2026 18:59
@moonbox3 moonbox3 added the .NET Issue or Pull requests regarding .NET code label May 23, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates Redis JSON mapping to persist only schema-annotated fields (excluding unannotated POCO properties), and adjusts unit tests to validate the new JSON payload shape.

Changes:

  • Build Redis JSON payload from DataProperties + VectorProperties rather than serializing the whole model.
  • Ensure unannotated properties (e.g., NotAnnotated) are not written to Redis JSON.
  • Update unit tests’ expected JSON and add assertions that unannotated fields are absent.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
dotnet/src/VectorData/Redis/RedisJsonMapper.cs Reworks JSON construction to include only schema-defined data/vector properties and exclude unannotated POCO properties.
dotnet/test/VectorData/Redis.UnitTests/RedisJsonMapperTests.cs Adds assertions ensuring unannotated fields are not present in the mapped JSON.
dotnet/test/VectorData/Redis.UnitTests/RedisJsonCollectionTests.cs Updates expected JSON payloads to no longer include NotAnnotated and removes obsolete TODOs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dotnet/test/VectorData/Redis.UnitTests/RedisJsonMapperTests.cs Outdated
Comment thread dotnet/test/VectorData/Redis.UnitTests/RedisJsonMapperTests.cs Outdated
Comment thread dotnet/src/VectorData/Redis/RedisJsonMapper.cs
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 93% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Design Approach


Automated review by jluocsa's agents

@jluocsa
Copy link
Copy Markdown
Member Author

jluocsa commented May 23, 2026

The three failing CI legs (dotnet-build-and-test (10.0, ubuntu-latest, Release, true, integration), (10.0, windows-latest, Debug), (9.0, windows-latest, Release)) are not caused by this PR — they're hitting a fresh NU1903 advisory on Scriban 7.1.0 (GHSA-24c8-4792-22hx, High / CVSS 8.7) that breaks every dotnet build --warnaserror run in the repo right now.

error NU1903: Package 'Scriban' 7.1.0 has a known high severity vulnerability,
https://github.com/advisories/GHSA-24c8-4792-22hx

I opened #14031 as a single-line bump to Scriban 7.2.0 (the patched version) — once that lands, this PR's CI should turn green on a re-run.

Per Copilot's review on microsoft#14030: replace the null-forgiving operator on the
mapped JsonObject with an explicit Assert.NotNull, then drop the '?' / '!'
operators on subsequent index access and ContainsKey. This keeps test
failures diagnostic (clean assertion failure instead of NullReferenceException)
and reads as a single contract: 'the mapper must return a JsonObject; here is
what it should contain'.
Per Copilot's review on microsoft#14030: collapse the two near-identical switch-expression + foreach-Span blocks for float and double vector payloads into a single type-switch that funnels every supported shape (ReadOnlyMemory<T>, Embedding<T>, T[]) through a shared AppendVector<T>(JsonArray, ReadOnlySpan<T>) helper. Behavior is unchanged: same set of accepted vector shapes, same ordering, same UnreachableException on an unsupported runtime type.
@westey-m westey-m self-assigned this May 26, 2026
@westey-m
Copy link
Copy Markdown
Contributor

@jluocsa, thanks for the contribution. This looks like a good improvement.
Note that we are in the process of moving the code to a new repo, which involves renaming the packages as well. The new repo should be live very soon (fingers crossed).
It would make sense to apply the fix in the new repo, rather than here. I'll ping you as soon as that's ready and we can hopefully get it in there instead.

CC @roji

@westey-m westey-m moved this to Community PR in Agent Framework May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Issue or Pull requests regarding .NET code

Projects

Status: Community PR

Development

Successfully merging this pull request may close these issues.

.Net: Bug: .NET: RedisJsonCollection JSON upsert includes unannotated POCO properties

4 participants