diff --git a/src/Wiki.Application/Encyclopedia/PlanParsing.cs b/src/Wiki.Application/Encyclopedia/PlanParsing.cs
index 34a2e8d..32e7333 100644
--- a/src/Wiki.Application/Encyclopedia/PlanParsing.cs
+++ b/src/Wiki.Application/Encyclopedia/PlanParsing.cs
@@ -26,12 +26,12 @@ internal static partial class PlanParsing
return text is not null && text.All(char.IsAsciiLetter) && Enum.TryParse(text, ignoreCase: true, out category);
}
- /// The JSON object in a reply, which may be wrapped in a code fence; null when it is not one.
+ /// The JSON object in a reply, which may be wrapped in a code fence; null when it is not one or repeats a property.
public static JsonObject? Object(string reply)
{
try
{
- return JsonNode.Parse(StripFence(reply)) as JsonObject;
+ return JsonNode.Parse(StripFence(reply), documentOptions: new JsonDocumentOptions { AllowDuplicateProperties = false }) as JsonObject;
}
catch (JsonException)
{
diff --git a/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs b/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs
index 87da135..90b7ea8 100644
--- a/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs
+++ b/test/Wiki.Application.Tests/AssignNotesUseCaseTests.cs
@@ -170,6 +170,20 @@ public class AssignNotesUseCaseTests
Assert.Empty(outcome.Assignments);
}
+ [Fact]
+ public async Task Assign_DuplicatePropertyInReply_IsUnusable()
+ {
+ var model = new FakeTextModel().Reply("""
+ {"new":[{"slug":"garden","titleDe":"G","titleEn":"G","titleEn":"H","category":"room","scope":"s"}],
+ "notes":[{"note":"a","articles":["garden"]}]}
+ """);
+
+ var outcome = await UseCase(model).AssignAsync(Batch("a"), [], ArticlePlan.Empty(), CancellationToken.None);
+
+ Assert.True(outcome.Unusable);
+ Assert.Empty(outcome.Assignments);
+ }
+
[Fact]
public async Task Assign_PromptIsBounded()
{