SQL Server 2025 CU4: Does JSON_CONTAINS Break Your Queries? Test Day 2

Before we dive into today's topic, if you missed my previous post:  SQL Server 2025 CU3 vs CU4: Has Microsoft Really Fixed the SESSION_CONTEXT Issue? Test Day 1 

👉 If you found this deep-dive helpful, feel free to check out the ads—your support helps me keep creating high-quality SQL Server content for the community.

SQL Server 2025 CU4: Does JSON_CONTAINS Break Your Queries? Test Day 2

In this post, I’ll show you why a seemingly minor fix to JSON_CONTAINS in SQL Server 2025 CU4 might suddenly break your production workloads. Don't let silent logic errors ruin your uptime. Here is how to test and bulletproof your code!

TL;DR

✔️ KB 5029490 limits the JSON_CONTAINS comparison mode strictly to 0 or 1 🛠️ ✔️ Mode 0 enforces an Exact Match, while Mode 1 allows Pattern Matching 🧩 ✔️ Parameterized queries face the highest risk of breaking changes upon upgrading to CU4 ⚠️

Hi SQL SERVER Guys,

We all know how important it is to make every second count when managing SQL Server environments. Unpredictable query execution is a DBA's worst nightmare . Today, for our Day 2 test, we are diving into a minor but critical update in SQL Server 2025 CU4 that fundamentally changes how the engine handles JSON validation. Let's dive into these game-changing features!

What it really is: The JSON_CONTAINS Fix

The JSON_CONTAINS function accepts an argument called "comparison mode". Before this fix (CU3 and earlier), the SQL Server engine was dangerously forgiving.

The Problem: You could pass "weird" values into the function, like JSON_CONTAINS(..., ..., 2) or -1. The result was undocumented behavior, inconsistent matching, and a high risk of silent bugs or regressions.

✔️ The Fix (CU4): Microsoft restricted the parameter to only two valid values:

  • 0 → Exact match (Default): The JSON must match perfectly. JSON_CONTAINS('{"a":1}', '{"a":1}', 0)TRUE JSON_CONTAINS('{"a":1}', '{"a":"1"}', 0)FALSE
  • 1 → Pattern match: More flexible, working similarly to a "contains / like" operator. JSON_CONTAINS('{"name":"Luca"}', '{"name":"Lu"}', 1)TRUE

Why is this important? 

This fix eliminates undefined behaviors, makes the function 100% deterministic, and prevents bugs that are nearly impossible to trace. However, the real-world impact is that if you had legacy code passing a 2, it will now throw an error or force strict behavior. Microsoft made the API "strict"—less silent flexibility equals more predictability.

🧪 1. Test CU3 vs CU4 on JSON_CONTAINS

☝We don't just blindly follow what Microsoft says. We double-check everything and run benchmarks to see what the real benefits are.

The goal here is to verify the engine's behavior with invalid values and spot breaking changes. 

Run this script on your instances:

PRINT '=== TEST SQL SERVER 2025 CU - JSON_CONTAINS MODES ===';

DECLARE @json JSON = N'{
    "info": {
        "name": "Luca Biondi",
        "tags": ["sql", "performance", "server-2025"]
    }
}';

-- ========================================================
-- TEST 1: MODE 0 (Exact Match) - Default
-- ========================================================
SELECT 
    JSON_CONTAINS(@json, N'{"name": "Luca Biondi"}', '$.info', 0) AS ExactMatch_OK,
    JSON_CONTAINS(@json, N'{"name": "Luca"}', '$.info', 0) AS ExactMatch_Fail; 
    -- Il secondo darà 0 perché "Luca" non è uguale a "Luca Biondi"

-- ========================================================
-- TEST 2: MODE 1 (Pattern Match / Partial)
-- ========================================================
SELECT 
    JSON_CONTAINS(@json, N'"sql"', '$.info.tags', 1) AS PatternMatch_Array,
    JSON_CONTAINS(@json, N'{"name": "Luca Biondi"}', '$.info', 1) AS PatternMatch_Object;

-- ========================================================
-- TEST 2: MODE 2 ??????? (Pattern Match / Partial)
-- ========================================================
SELECT 
    JSON_CONTAINS(@json, N'"sql"', '$.info.tags', 1) AS PatternMatch_Array,
    JSON_CONTAINS(@json, N'{"name": "Luca Biondi"}', '$.info', 2) AS PatternMatch_Object;

What to expect:

  • 🔴 CU3 (Before Fix): Passing mode 2, -1, or 999 might work randomly, behave like 0 or 1, or yield inconsistent results. Highly non-deterministic.

  • 🟢 CU4 (After Fix): Modes 0 and 1 work perfectly. 
  • Any other value results in an explicit ❌ error. The behavior is finally stable.

  • Any values different than 0 and 1 bring the error message:
Msg 13692, Level 16, State 1, Line 32
The comparison_mode argument of JSON_CONTAINS must be 0 or 1.

💣 2. Borderline Cases (The Dangerous Ones)

Here are the scenarios that will bite you if you don't audit your code before upgrading:

  • 💣 Case 1 — The "Phantom" Mode: JSON_CONTAINS(@json, N'{"name":"Lu"}', 2). In CU3, this could accidentally act like a pattern match. In CU4, it throws an invalid argument error.
  • 💣 Case 2 — Silent Bug in Production: Passing a hardcoded variable DECLARE @mode INT = 2; worked (poorly) in CU3 but will instantly break everything in CU4.
  • 💣 Case 3 — Parameterization (The Sneakiest): If a stored procedure accepts @mode INT from an application, and the app sends a 2, CU3 handled it unpredictably. CU4 triggers a runtime error, potentially taking down your app.
  • 💣 Case 4 — Inconsistent Query Plans: In CU3, invalid modes could generate wildly different execution plans and mess with cardinality estimates, destroying performance. CU4 stops this at the gate.
  • 💣 Case 5 — Nested JSON + Pattern: Searching a nested structure with mode 2 yielded random TRUE/FALSE results in CU3. CU4 enforces strict, coherent logic.

👉 “CU4 fixes a subtle but dangerous issue in JSON_CONTAINS by enforcing strict comparison modes, eliminating undefined behavior that could previously lead to silent logic errors.”

🚀 My REAL Strategy

In my experience, you cannot blindly apply cumulative updates without auditing your codebase for undocumented parameter usage. Before deploying CU4, run a text search across your sys.sql_modules for JSON_CONTAINS. Pay special attention to stored procedures where the comparison mode is passed as a variable (@mode). Add a validation block (IF @mode NOT IN (0,1) THROW...) immediately so your application handles the exception gracefully instead of crashing abruptly at the query execution level.

Official Sources:


📢 Support the Blog: Did you find this deep-dive helpful? The ads you see here are selected to reflect your interests. If a partner's offer catches your eye, give it a look! Your engagement helps me continue publishing premium SQL Server content for the community.

Biondi Luca @2026 - Sharing over 25 years of Gained Knowledge for Passion.

Comments

I Post più popolari

Speaking to Sql Server, sniffing the TDS protocol

SQL Server, find text in a Trigger, Stored Procedures, View and Function. Two ways and what ways is better

SQL Server, Avoid that damn Table Spool!