diff --git a/.claude/skills/migrate-groovy-to-java/SKILL.md b/.claude/skills/migrate-groovy-to-java/SKILL.md
index d75b8134607..c603bf227b6 100644
--- a/.claude/skills/migrate-groovy-to-java/SKILL.md
+++ b/.claude/skills/migrate-groovy-to-java/SKILL.md
@@ -32,10 +32,6 @@ When converting Groovy code to Java code, make sure that:
 - When translating Spock `Mock(...)` usage, use `libs.bundles.mockito` instead of writing manual recording/stub implementations
 
 TableTest usage
-  Dependency, if missing add:
-    - Groovy: testImplementation libs.tabletest
-    - Kotlin: testImplementation(libs.tabletest)
-
   Import: `import org.tabletest.junit.TableTest;`
 
   JDK 8 rules:
diff --git a/.claude/skills/migrate-junit-source-to-tabletest/SKILL.md b/.claude/skills/migrate-junit-source-to-tabletest/SKILL.md
index ae4008039c5..062abd87d72 100644
--- a/.claude/skills/migrate-junit-source-to-tabletest/SKILL.md
+++ b/.claude/skills/migrate-junit-source-to-tabletest/SKILL.md
@@ -11,11 +11,6 @@ Process (do in this order):
 4) Write each modified file once in full using Write (no incremental per-test edits).
 5) Run module tests once and verify "BUILD SUCCESSFUL". If failed, inspect JUnit XML report.
 
-Dependency:
-- If missing, add:
-  - Groovy: testImplementation libs.tabletest
-  - Kotlin: testImplementation(libs.tabletest)
-
 Import: `import org.tabletest.junit.TableTest;`
 
 JDK 8 rules:
@@ -34,6 +29,7 @@ Table formatting rules (mandatory):
 - Use '|' as delimiter.
 - Align columns with spaces so pipes line up vertically.
 - Prefer single quotes for strings requiring quotes (e.g., 'a|b', '[]', '{}', ' ').
+- Use value sets (`{a, b, c}`) instead of matrix-style repetition when only one dimension varies across otherwise-identical rows.
 
 Conversions:
 A) @CsvSource
@@ -42,7 +38,8 @@ A) @CsvSource
 - If delimiter is ',' (default): replace ',' with '|' in rows.
 
 B) @ValueSource
-- Convert to @TableTest with header from parameter name.
+- Keep single-parameter tests on `@ValueSource` (and `@NullSource` when null cases are needed).
+- Otherwise convert to @TableTest with header from parameter name.
 - Each value becomes one row.
 - Add "scenario" column using common sense for name.
 
@@ -60,6 +57,11 @@ C) @MethodSource (convert only if values are representable as strings)
 - '' = empty string.
 - For String params that start with '[' or '{', quote to avoid collection parsing (prefer '[]'/'{}').
 
+D) @TypeConverter
+- Use `@TypeConverter` for symbolic constants used by migrated table rows (e.g. `Long.MAX_VALUE`, `DDSpanId.MAX`).
+- Prefer explicit one-case-one-return mappings.
+- Prefer shared converter utilities (e.g. in `utils/test-utils`) when reuse across modules is likely.
+
 Scenario handling:
 - If MethodSource includes a leading description string OR @ParameterizedTest(name=...) uses {0}, convert that to a scenario column and remove that parameter from method signature.
 
diff --git a/components/json/build.gradle.kts b/components/json/build.gradle.kts
index fda68a66ba8..ce67f74ff29 100644
--- a/components/json/build.gradle.kts
+++ b/components/json/build.gradle.kts
@@ -7,7 +7,3 @@ apply(from = "$rootDir/gradle/java.gradle")
 jmh {
   jmhVersion = libs.versions.jmh.get()
 }
-
-dependencies {
-  testImplementation(libs.tabletest)
-}
diff --git a/components/json/src/test/java/datadog/json/JsonMapperTest.java b/components/json/src/test/java/datadog/json/JsonMapperTest.java
index d6cacd83b25..ffe973e52ca 100644
--- a/components/json/src/test/java/datadog/json/JsonMapperTest.java
+++ b/components/json/src/test/java/datadog/json/JsonMapperTest.java
@@ -17,6 +17,8 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.tabletest.junit.Scenario;
 import org.tabletest.junit.TableTest;
 
@@ -77,28 +79,16 @@ static Stream<Arguments> testMappingToJsonObjectArguments() {
             "{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true,\"key8\":\"toString\"}"));
   }
 
-  @TableTest({
-    "Scenario     | Json  ",
-    "null         |       ",
-    "null string  | 'null'",
-    "empty string | ''    ",
-    "empty object | '{}'  "
-  })
   @ParameterizedTest(name = "test mapping to Map from empty JSON object: {0}")
+  @NullSource
+  @ValueSource(strings = {"null", "", "{}"})
   void testMappingToMapFromEmptyJsonObject(String json) throws IOException {
     Map<String, Object> parsed = JsonMapper.fromJsonToMap(json);
     assertEquals(emptyMap(), parsed);
   }
 
-  // temporary disable spotless, will open issue to fix this.
-  // spotless:off
-  @TableTest({
-      "Scenario | Json  ",
-      "integer  | 1     ",
-      "array    | [1, 2]"
-  })
-  // spotless:on
   @ParameterizedTest(name = "test mapping to Map from non-object JSON: {0}")
+  @ValueSource(strings = {"1", "[1, 2]"})
   void testMappingToMapFromNonObjectJson(String json) {
     assertThrows(IOException.class, () -> JsonMapper.fromJsonToMap(json));
   }
@@ -138,14 +128,9 @@ void testMappingArrayToJsonArray(String ignoredScenario, String[] input, String
     assertArrayEquals(input != null ? input : new String[] {}, parsed);
   }
 
-  @TableTest({
-    "Scenario      | Json  ",
-    "null          |       ",
-    "null string   | 'null'",
-    "empty string  | ''    ",
-    "empty array   | '[]'  "
-  })
   @ParameterizedTest(name = "test mapping to List from empty JSON object: {0}")
+  @NullSource
+  @ValueSource(strings = {"null", "", "[]"})
   void testMappingToListFromEmptyJsonObject(String json) throws IOException {
     List<String> parsed = JsonMapper.fromJsonToList(json);
     assertEquals(emptyList(), parsed);
diff --git a/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java b/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java
index e6cc4719b88..38d30987c91 100644
--- a/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java
+++ b/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java
@@ -7,58 +7,61 @@
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
-import java.math.BigInteger;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.stream.Stream;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.tabletest.junit.TableTest;
+import org.tabletest.junit.TypeConverterSources;
 
+@TypeConverterSources(DDTraceApiTableTestConverters.class)
 class DDSpanIdTest {
 
-  @ParameterizedTest(name = "convert ids from/to String {0}")
-  @MethodSource("convertIdsFromToStringArguments")
-  void convertIdsFromToString(String displayName, String stringId, long expectedId) {
+  @TableTest({
+    "scenario           | stringId               | expectedId          ",
+    "zero               | '0'                    | 0                   ",
+    "one                | '1'                    | 1                   ",
+    "max                | '18446744073709551615' | DDSpanId.MAX        ",
+    "long max           | '9223372036854775807'  | Long.MAX_VALUE      ",
+    "long max plus one  | '9223372036854775808'  | Long.MIN_VALUE      "
+  })
+  @ParameterizedTest(name = "convert ids from/to String [{index}]")
+  void convertIdsFromToString(String stringId, long expectedId) {
     long ddid = DDSpanId.from(stringId);
 
     assertEquals(expectedId, ddid);
     assertEquals(stringId, DDSpanId.toString(ddid));
   }
 
-  static Stream<Arguments> convertIdsFromToStringArguments() {
-    return Stream.of(
-        Arguments.of("zero", "0", 0L),
-        Arguments.of("one", "1", 1L),
-        Arguments.of("max", "18446744073709551615", DDSpanId.MAX),
-        Arguments.of("long max", String.valueOf(Long.MAX_VALUE), Long.MAX_VALUE),
-        Arguments.of(
-            "long max plus one",
-            BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE).toString(),
-            Long.MIN_VALUE));
-  }
-
-  @ParameterizedTest(name = "fail on illegal String {0}")
-  @MethodSource("failOnIllegalStringArguments")
+  @ParameterizedTest(name = "fail on illegal String [{index}]")
+  @NullSource
+  @ValueSource(
+      strings = {
+        "",
+        "-1",
+        "18446744073709551616",
+        "18446744073709551625",
+        "184467440737095516150",
+        "18446744073709551a1",
+        "184467440737095511a"
+      })
   void failOnIllegalString(String stringId) {
     assertThrows(NumberFormatException.class, () -> DDSpanId.from(stringId));
   }
 
-  static Stream<Arguments> failOnIllegalStringArguments() {
-    return Stream.of(
-        Arguments.of((Object) null),
-        Arguments.of(""),
-        Arguments.of("-1"),
-        Arguments.of("18446744073709551616"),
-        Arguments.of("18446744073709551625"),
-        Arguments.of("184467440737095516150"),
-        Arguments.of("18446744073709551a1"),
-        Arguments.of("184467440737095511a"));
-  }
-
-  @ParameterizedTest(name = "convert ids from/to hex String {0}")
-  @MethodSource("convertIdsFromToHexStringArguments")
+  @TableTest({
+    "scenario                    | hexId                  | expectedId          ",
+    "zero                        | '0'                    | 0                   ",
+    "one                         | '1'                    | 1                   ",
+    "max                         | 'ffffffffffffffff'     | DDSpanId.MAX        ",
+    "long max                    | '7fffffffffffffff'     | Long.MAX_VALUE      ",
+    "long min                    | '8000000000000000'     | Long.MIN_VALUE      ",
+    "long min with leading zeros | '00008000000000000000' | Long.MIN_VALUE      ",
+    "hex sample                  | 'cafebabe'             | 3405691582          ",
+    "fifteen hex digits          | '123456789abcdef'      | 81985529216486895   "
+  })
+  @ParameterizedTest(name = "convert ids from/to hex String [{index}]")
   void convertIdsFromToHexString(String hexId, long expectedId) {
     long ddid = DDSpanId.fromHex(hexId);
     String padded16 =
@@ -73,27 +76,22 @@ void convertIdsFromToHexString(String hexId, long expectedId) {
     assertEquals(padded16, DDSpanId.toHexStringPadded(ddid));
   }
 
-  static Stream<Arguments> convertIdsFromToHexStringArguments() {
-    return Stream.of(
-        Arguments.of("0", 0L),
-        Arguments.of("1", 1L),
-        Arguments.of(repeat("f", 16), DDSpanId.MAX),
-        Arguments.of("7" + repeat("f", 15), Long.MAX_VALUE),
-        Arguments.of("8" + repeat("0", 15), Long.MIN_VALUE),
-        Arguments.of(repeat("0", 4) + "8" + repeat("0", 15), Long.MIN_VALUE),
-        Arguments.of("cafebabe", 3405691582L),
-        Arguments.of("123456789abcdef", 81985529216486895L));
-  }
-
-  @ParameterizedTest(name = "convert ids from part of hex String {0}")
-  @MethodSource("convertIdsFromPartOfHexStringArguments")
+  @TableTest({
+    "scenario                                | hexId               | start | length | lowerCaseOnly | expectedId   ",
+    "null input                              |                     | 1     | 1      | false         |              ",
+    "empty input                             | ''                  | 1     | 1      | false         |              ",
+    "negative start                          | '00'                | -1    | 1      | false         |              ",
+    "zero length                             | '00'                | 0     | 0      | false         |              ",
+    "single zero at index 0                  | '00'                | 0     | 1      | false         | DDSpanId.ZERO",
+    "single zero at index 1                  | '00'                | 1     | 1      | false         | DDSpanId.ZERO",
+    "single zero at index 1 duplicate        | '00'                | 1     | 1      | false         | DDSpanId.ZERO",
+    "max lower-case                          | 'ffffffffffffffff'  | 0     | 16     | true          | DDSpanId.MAX ",
+    "upper-case rejected when lower-case only| 'ffffffffffffFfff'  | 0     | 16     | true          |              ",
+    "upper-case accepted when lower disabled | 'ffffffffffffFfff'  | 0     | 16     | false         | DDSpanId.MAX "
+  })
+  @ParameterizedTest(name = "convert ids from part of hex String [{index}]")
   void convertIdsFromPartOfHexString(
-      String displayName,
-      String hexId,
-      int start,
-      int length,
-      boolean lowerCaseOnly,
-      Long expectedId) {
+      String hexId, int start, int length, boolean lowerCaseOnly, Long expectedId) {
     Long parsedId = null;
     try {
       parsedId = DDSpanId.fromHex(hexId, start, length, lowerCaseOnly);
@@ -108,48 +106,13 @@ void convertIdsFromPartOfHexString(
     }
   }
 
-  static Stream<Arguments> convertIdsFromPartOfHexStringArguments() {
-    return Stream.of(
-        Arguments.of("null input", null, 1, 1, false, null),
-        Arguments.of("empty input", "", 1, 1, false, null),
-        Arguments.of("negative start", "00", -1, 1, false, null),
-        Arguments.of("zero length", "00", 0, 0, false, null),
-        Arguments.of("single zero at index 0", "00", 0, 1, false, DDSpanId.ZERO),
-        Arguments.of("single zero at index 1", "00", 1, 1, false, DDSpanId.ZERO),
-        Arguments.of("single zero at index 1 duplicate", "00", 1, 1, false, DDSpanId.ZERO),
-        Arguments.of("max lower-case", repeat("f", 16), 0, 16, true, DDSpanId.MAX),
-        Arguments.of(
-            "upper-case rejected when lower-case only",
-            repeat("f", 12) + "Ffff",
-            0,
-            16,
-            true,
-            null),
-        Arguments.of(
-            "upper-case accepted when lower-case disabled",
-            repeat("f", 12) + "Ffff",
-            0,
-            16,
-            false,
-            DDSpanId.MAX));
-  }
-
-  @ParameterizedTest(name = "fail on illegal hex String {0}")
-  @MethodSource("failOnIllegalHexStringArguments")
+  @ParameterizedTest(name = "fail on illegal hex String [{index}]")
+  @NullSource
+  @ValueSource(strings = {"", "-1", "10000000000000000", "ffffffffffffffzf", "fffffffffffffffz"})
   void failOnIllegalHexString(String hexId) {
     assertThrows(NumberFormatException.class, () -> DDSpanId.fromHex(hexId));
   }
 
-  static Stream<Arguments> failOnIllegalHexStringArguments() {
-    return Stream.of(
-        Arguments.of((Object) null),
-        Arguments.of(""),
-        Arguments.of("-1"),
-        Arguments.of("1" + repeat("0", 16)),
-        Arguments.of(repeat("f", 14) + "zf"),
-        Arguments.of(repeat("f", 15) + "z"));
-  }
-
   @ParameterizedTest(name = "generate id with {0}")
   @ValueSource(strings = {"RANDOM", "SEQUENTIAL", "SECURE_RANDOM"})
   void generateIdWithStrategy(String strategyName) {
diff --git a/dd-trace-api/src/test/java/datadog/trace/api/DDTraceApiTableTestConverters.java b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceApiTableTestConverters.java
new file mode 100644
index 00000000000..e277fe8e2e7
--- /dev/null
+++ b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceApiTableTestConverters.java
@@ -0,0 +1,51 @@
+package datadog.trace.api;
+
+import datadog.trace.test.util.TableTestTypeConverters;
+import org.tabletest.junit.TypeConverter;
+
+/** TableTest converters shared by dd-trace-api test classes for unparsable constants. */
+public final class DDTraceApiTableTestConverters {
+
+  private DDTraceApiTableTestConverters() {}
+
+  @TypeConverter
+  public static long toLong(String value) {
+    if (value == null) {
+      throw new IllegalArgumentException("Value cannot be null");
+    }
+    String token = value.trim();
+    switch (token) {
+      case "DDSpanId.MAX":
+        return DDSpanId.MAX;
+      case "DDSpanId.ZERO":
+        return DDSpanId.ZERO;
+      default:
+        return TableTestTypeConverters.toLong(token);
+    }
+  }
+
+  @TypeConverter
+  public static DD64bTraceId toDD64bTraceId(String value) {
+    if (value == null) {
+      throw new IllegalArgumentException("Value cannot be null");
+    }
+    switch (value.trim()) {
+      case "DD64bTraceId.ZERO":
+        return DD64bTraceId.from(0L);
+      case "DD64bTraceId.ONE":
+        return DD64bTraceId.from(1L);
+      case "DD64bTraceId.MAX":
+        return DD64bTraceId.MAX;
+      case "DD64bTraceId.LONG_MAX":
+        return DD64bTraceId.from(Long.MAX_VALUE);
+      case "DD64bTraceId.LONG_MIN":
+        return DD64bTraceId.from(Long.MIN_VALUE);
+      case "DD64bTraceId.CAFEBABE":
+        return DD64bTraceId.from(3405691582L);
+      case "DD64bTraceId.HEX":
+        return DD64bTraceId.from(81985529216486895L);
+      default:
+        throw new IllegalArgumentException("Unsupported DD64bTraceId token: " + value);
+    }
+  }
+}
diff --git a/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java
index 176b0aeed3f..3dcec4d583b 100644
--- a/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java
+++ b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java
@@ -5,24 +5,29 @@
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
-import java.math.BigInteger;
-import java.util.stream.Stream;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.tabletest.junit.TableTest;
+import org.tabletest.junit.TypeConverterSources;
 
+@TypeConverterSources(DDTraceApiTableTestConverters.class)
 class DDTraceIdTest {
 
-  @ParameterizedTest(name = "convert 64-bit ids from/to long {1} and check strings")
-  @MethodSource("convert64BitIdsFromToLongAndCheckStringsArguments")
+  @TableTest({
+    "scenario   | longId               | expectedString        | expectedHex                       ",
+    "zero       | 0                    | '0'                   | '00000000000000000000000000000000'",
+    "one        | 1                    | '1'                   | '00000000000000000000000000000001'",
+    "minus one  | -1                   | '18446744073709551615'| '0000000000000000ffffffffffffffff'",
+    "long max   | Long.MAX_VALUE       | '9223372036854775807' | '00000000000000007fffffffffffffff'",
+    "long min   | Long.MIN_VALUE       | '9223372036854775808' | '00000000000000008000000000000000'"
+  })
+  @ParameterizedTest(name = "convert 64-bit ids from/to long and check strings [{index}]")
   void convert64BitIdsFromToLongAndCheckStrings(
-      String displayName,
-      long longId,
-      DD64bTraceId expectedId,
-      String expectedString,
-      String expectedHex) {
+      long longId, String expectedString, String expectedHex) {
     DD64bTraceId ddid = DD64bTraceId.from(longId);
+    DD64bTraceId expectedId = DD64bTraceId.from(expectedString);
     DDTraceId defaultDdid = DDTraceId.from(longId);
 
     assertEquals(expectedId, ddid);
@@ -33,72 +38,50 @@ void convert64BitIdsFromToLongAndCheckStrings(
     assertEquals(expectedHex, ddid.toHexString());
   }
 
-  static Stream<Arguments> convert64BitIdsFromToLongAndCheckStringsArguments() {
-    return Stream.of(
-        Arguments.of("zero", 0L, DD64bTraceId.ZERO, "0", repeat("0", 32)),
-        Arguments.of("one", 1L, DD64bTraceId.ONE, "1", repeat("0", 31) + "1"),
-        Arguments.of(
-            "minus one",
-            -1L,
-            DD64bTraceId.MAX,
-            "18446744073709551615",
-            repeat("0", 16) + repeat("f", 16)),
-        Arguments.of(
-            "long max",
-            Long.MAX_VALUE,
-            DD64bTraceId.from(Long.MAX_VALUE),
-            "9223372036854775807",
-            repeat("0", 16) + "7" + repeat("f", 15)),
-        Arguments.of(
-            "long min",
-            Long.MIN_VALUE,
-            DD64bTraceId.from(Long.MIN_VALUE),
-            "9223372036854775808",
-            repeat("0", 16) + "8" + repeat("0", 15)));
-  }
-
-  @ParameterizedTest(name = "convert 64-bit ids from/to String representation: {1}")
-  @MethodSource("convert64BitIdsFromToStringRepresentationArguments")
-  void convert64BitIdsFromToStringRepresentation(
-      String displayName, String stringId, DD64bTraceId expectedId) {
+  @TableTest({
+    "scenario           | stringId               | expectedId           ",
+    "zero               | '0'                    | DD64bTraceId.ZERO    ",
+    "one                | '1'                    | DD64bTraceId.ONE     ",
+    "max                | '18446744073709551615' | DD64bTraceId.MAX     ",
+    "long max           | '9223372036854775807'  | DD64bTraceId.LONG_MAX",
+    "long max plus one  | '9223372036854775808'  | DD64bTraceId.LONG_MIN"
+  })
+  @ParameterizedTest(name = "convert 64-bit ids from/to String representation [{index}]")
+  void convert64BitIdsFromToStringRepresentation(String stringId, DD64bTraceId expectedId) {
     DD64bTraceId ddid = DD64bTraceId.from(stringId);
 
     assertEquals(expectedId, ddid);
     assertEquals(stringId, ddid.toString());
   }
 
-  static Stream<Arguments> convert64BitIdsFromToStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of("zero", "0", DD64bTraceId.ZERO),
-        Arguments.of("one", "1", DD64bTraceId.ONE),
-        Arguments.of("max", "18446744073709551615", DD64bTraceId.MAX),
-        Arguments.of("long max", String.valueOf(Long.MAX_VALUE), DD64bTraceId.from(Long.MAX_VALUE)),
-        Arguments.of(
-            "long max plus one",
-            BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE).toString(),
-            DD64bTraceId.from(Long.MIN_VALUE)));
-  }
-
-  @ParameterizedTest(name = "fail parsing illegal 64-bit id String representation: {0}")
-  @MethodSource("failParsingIllegal64BitIdStringRepresentationArguments")
+  @ParameterizedTest(name = "fail parsing illegal 64-bit id String representation [{index}]")
+  @NullSource
+  @ValueSource(
+      strings = {
+        "",
+        "-1",
+        "18446744073709551616",
+        "18446744073709551625",
+        "184467440737095516150",
+        "18446744073709551a1",
+        "184467440737095511a"
+      })
   void failParsingIllegal64BitIdStringRepresentation(String stringId) {
     assertThrows(NumberFormatException.class, () -> DD64bTraceId.from(stringId));
   }
 
-  static Stream<Arguments> failParsingIllegal64BitIdStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of((Object) null),
-        Arguments.of(""),
-        Arguments.of("-1"),
-        Arguments.of("18446744073709551616"),
-        Arguments.of("18446744073709551625"),
-        Arguments.of("184467440737095516150"),
-        Arguments.of("18446744073709551a1"),
-        Arguments.of("184467440737095511a"));
-  }
-
-  @ParameterizedTest(name = "convert 64-bit ids from/to hex String representation: {0}")
-  @MethodSource("convert64BitIdsFromToHexStringRepresentationArguments")
+  @TableTest({
+    "scenario                    | hexId                  | expectedId           ",
+    "zero                        | '0'                    | DD64bTraceId.ZERO    ",
+    "one                         | '1'                    | DD64bTraceId.ONE     ",
+    "max                         | 'ffffffffffffffff'     | DD64bTraceId.MAX     ",
+    "long max                    | '7fffffffffffffff'     | DD64bTraceId.LONG_MAX",
+    "long min                    | '8000000000000000'     | DD64bTraceId.LONG_MIN",
+    "long min with leading zeros | '00008000000000000000' | DD64bTraceId.LONG_MIN",
+    "hex sample                  | 'cafebabe'             | DD64bTraceId.CAFEBABE",
+    "fifteen hex digits          | '123456789abcdef'      | DD64bTraceId.HEX     "
+  })
+  @ParameterizedTest(name = "convert 64-bit ids from/to hex String representation [{index}]")
   void convert64BitIdsFromToHexStringRepresentation(String hexId, DD64bTraceId expectedId) {
     DD64bTraceId ddid = DD64bTraceId.fromHex(hexId);
     String padded16 =
@@ -111,38 +94,39 @@ void convert64BitIdsFromToHexStringRepresentation(String hexId, DD64bTraceId exp
     assertEquals(padded32, ddid.toHexStringPadded(32));
   }
 
-  static Stream<Arguments> convert64BitIdsFromToHexStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of("0", DD64bTraceId.ZERO),
-        Arguments.of("1", DD64bTraceId.ONE),
-        Arguments.of(repeat("f", 16), DD64bTraceId.MAX),
-        Arguments.of("7" + repeat("f", 15), DD64bTraceId.from(Long.MAX_VALUE)),
-        Arguments.of("8" + repeat("0", 15), DD64bTraceId.from(Long.MIN_VALUE)),
-        Arguments.of(repeat("0", 4) + "8" + repeat("0", 15), DD64bTraceId.from(Long.MIN_VALUE)),
-        Arguments.of("cafebabe", DD64bTraceId.from(3405691582L)),
-        Arguments.of("123456789abcdef", DD64bTraceId.from(81985529216486895L)));
-  }
-
-  @ParameterizedTest(name = "fail parsing illegal 64-bit hexadecimal String representation: {0}")
-  @MethodSource("failParsingIllegal64BitHexadecimalStringRepresentationArguments")
+  @ParameterizedTest(
+      name = "fail parsing illegal 64-bit hexadecimal String representation [{index}]")
+  @NullSource
+  @ValueSource(strings = {"", "-1", "10000000000000000", "ffffffffffffffzf", "fffffffffffffffz"})
   void failParsingIllegal64BitHexadecimalStringRepresentation(String hexId) {
     assertThrows(NumberFormatException.class, () -> DD64bTraceId.fromHex(hexId));
   }
 
-  static Stream<Arguments> failParsingIllegal64BitHexadecimalStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of((Object) null),
-        Arguments.of(""),
-        Arguments.of("-1"),
-        Arguments.of("1" + repeat("0", 16)),
-        Arguments.of(repeat("f", 14) + "zf"),
-        Arguments.of(repeat("f", 15) + "z"));
-  }
-
-  @ParameterizedTest(name = "convert 128-bit ids from/to hexadecimal String representation {3}")
-  @MethodSource("convert128BitIdsFromToHexadecimalStringRepresentationArguments")
+  @TableTest({
+    "scenario                      | highOrderBits        | lowOrderBits         | hexId                             ",
+    "both long min                 | Long.MIN_VALUE       | Long.MIN_VALUE       | '80000000000000008000000000000000'",
+    "high long min low one         | Long.MIN_VALUE       | 1                    | '80000000000000000000000000000001'",
+    "high long min low long max    | Long.MIN_VALUE       | Long.MAX_VALUE       | '80000000000000007fffffffffffffff'",
+    "high one low long min         | 1                    | Long.MIN_VALUE       | '00000000000000018000000000000000'",
+    "high one low one              | 1                    | 1                    | '00000000000000010000000000000001'",
+    "high one low long max         | 1                    | Long.MAX_VALUE       | '00000000000000017fffffffffffffff'",
+    "high long max low long min    | Long.MAX_VALUE       | Long.MIN_VALUE       | '7fffffffffffffff8000000000000000'",
+    "high long max low one         | Long.MAX_VALUE       | 1                    | '7fffffffffffffff0000000000000001'",
+    "high long max low long max    | Long.MAX_VALUE       | Long.MAX_VALUE       | '7fffffffffffffff7fffffffffffffff'",
+    "all zeros length one          | 0                    | 0                    | '0'                               ",
+    "all zeros length sixteen      | 0                    | 0                    | '0000000000000000'                ",
+    "all zeros length seventeen    | 0                    | 0                    | '00000000000000000'               ",
+    "all zeros length thirty-two   | 0                    | 0                    | '00000000000000000000000000000000'",
+    "low fifteen                   | 0                    | 15                   | 'f'                               ",
+    "low minus one                 | 0                    | -1                   | 'ffffffffffffffff'                ",
+    "high fifteen low minus one    | 15                   | -1                   | 'fffffffffffffffff'               ",
+    "all f                         | -1                   | -1                   | 'ffffffffffffffffffffffffffffffff'",
+    "hex literal                   | 1311768467463790320  | 1311768467463790320  | '123456789abcdef0123456789abcdef0'"
+  })
+  @ParameterizedTest(
+      name = "convert 128-bit ids from/to hexadecimal String representation [{index}]")
   void convert128BitIdsFromToHexadecimalStringRepresentation(
-      String displayName, long highOrderBits, long lowOrderBits, String hexId) {
+      long highOrderBits, long lowOrderBits, String hexId) {
     DDTraceId parsedId = DD128bTraceId.fromHex(hexId);
     DDTraceId id = DD128bTraceId.from(highOrderBits, lowOrderBits);
     String paddedHexId = leftPadWithZeros(hexId, 32);
@@ -156,110 +140,38 @@ void convert128BitIdsFromToHexadecimalStringRepresentation(
     assertEquals(Long.toUnsignedString(lowOrderBits), parsedId.toString());
   }
 
-  static Stream<Arguments> convert128BitIdsFromToHexadecimalStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of(
-            "both long min",
-            Long.MIN_VALUE,
-            Long.MIN_VALUE,
-            "8" + repeat("0", 15) + "8" + repeat("0", 15)),
-        Arguments.of(
-            "high long min low one",
-            Long.MIN_VALUE,
-            1L,
-            "8" + repeat("0", 15) + repeat("0", 15) + "1"),
-        Arguments.of(
-            "high long min low long max",
-            Long.MIN_VALUE,
-            Long.MAX_VALUE,
-            "8" + repeat("0", 15) + "7" + repeat("f", 15)),
-        Arguments.of(
-            "high one low long min",
-            1L,
-            Long.MIN_VALUE,
-            repeat("0", 15) + "1" + "8" + repeat("0", 15)),
-        Arguments.of("high one low one", 1L, 1L, repeat("0", 15) + "1" + repeat("0", 15) + "1"),
-        Arguments.of(
-            "high one low long max",
-            1L,
-            Long.MAX_VALUE,
-            repeat("0", 15) + "1" + "7" + repeat("f", 15)),
-        Arguments.of(
-            "high long max low long min",
-            Long.MAX_VALUE,
-            Long.MIN_VALUE,
-            "7" + repeat("f", 15) + "8" + repeat("0", 15)),
-        Arguments.of(
-            "high long max low one",
-            Long.MAX_VALUE,
-            1L,
-            "7" + repeat("f", 15) + repeat("0", 15) + "1"),
-        Arguments.of(
-            "high long max low long max",
-            Long.MAX_VALUE,
-            Long.MAX_VALUE,
-            "7" + repeat("f", 15) + "7" + repeat("f", 15)),
-        Arguments.of("all zeros length one", 0L, 0L, repeat("0", 1)),
-        Arguments.of("all zeros length sixteen", 0L, 0L, repeat("0", 16)),
-        Arguments.of("all zeros length seventeen", 0L, 0L, repeat("0", 17)),
-        Arguments.of("all zeros length thirty-two", 0L, 0L, repeat("0", 32)),
-        Arguments.of("low fifteen", 0L, 15L, repeat("f", 1)),
-        Arguments.of("low minus one", 0L, -1L, repeat("f", 16)),
-        Arguments.of("high fifteen low minus one", 15L, -1L, repeat("f", 17)),
-        Arguments.of("all f", -1L, -1L, repeat("f", 32)),
-        Arguments.of(
-            "hex literal",
-            1311768467463790320L,
-            1311768467463790320L,
-            "123456789abcdef0123456789abcdef0"));
-  }
-
   @ParameterizedTest(
-      name = "fail parsing illegal 128-bit id hexadecimal String representation: {0}")
-  @MethodSource("failParsingIllegal128BitIdHexadecimalStringRepresentationArguments")
+      name = "fail parsing illegal 128-bit id hexadecimal String representation [{index}]")
+  @NullSource
+  @ValueSource(strings = {"", "-1", "-A", "111111111111111111111111111111111", "123ABC", "123abcg"})
   void failParsingIllegal128BitIdHexadecimalStringRepresentation(String hexId) {
     assertThrows(NumberFormatException.class, () -> DD128bTraceId.fromHex(hexId));
   }
 
-  static Stream<Arguments> failParsingIllegal128BitIdHexadecimalStringRepresentationArguments() {
-    return Stream.of(
-        Arguments.of((Object) null),
-        Arguments.of(""),
-        Arguments.of("-1"),
-        Arguments.of("-A"),
-        Arguments.of(repeat("1", 33)),
-        Arguments.of("123ABC"),
-        Arguments.of("123abcg"));
-  }
-
+  @TableTest({
+    "scenario             | hexId                               | start | length | lowerCaseOnly",
+    "null string          |                                     | 0     | 0      | true         ",
+    "empty string         | ''                                  | 0     | 0      | true         ",
+    "out of bound length  | '123456789abcdef0'                  | 0     | 17     | true         ",
+    "out of bound end     | '123456789abcdef0'                  | 7     | 10     | true         ",
+    "out of bound start   | '123456789abcdef0'                  | 17    | 0      | true         ",
+    "invalid minus one    | '-1'                                | 0     | 1      | true         ",
+    "invalid minus a      | '-a'                                | 0     | 1      | true         ",
+    "invalid character    | '123abcg'                           | 0     | 7      | true         ",
+    "invalid upper case A | 'A'                                 | 0     | 1      | true         ",
+    "invalid upper case   | '123ABC'                            | 0     | 6      | true         ",
+    "too long             | '111111111111111111111111111111111' | 0     | 33     | true         "
+  })
   @ParameterizedTest(
       name =
-          "fail parsing illegal 128-bit id hexadecimal String representation from partial String: {1}")
-  @MethodSource(
-      "failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialStringArguments")
+          "fail parsing illegal 128-bit id hexadecimal String representation from partial String [{index}]")
   void failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialString(
-      String displayName, String hexId, int start, int length, boolean lowerCaseOnly) {
+      String hexId, int start, int length, boolean lowerCaseOnly) {
     assertThrows(
         NumberFormatException.class,
         () -> DD128bTraceId.fromHex(hexId, start, length, lowerCaseOnly));
   }
 
-  static Stream<Arguments>
-      failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialStringArguments() {
-    return Stream.of(
-        Arguments.of("null string", null, 0, 0, true),
-        Arguments.of("empty string", "", 0, 0, true),
-        Arguments.of("out of bound length", "123456789abcdef0", 0, 17, true),
-        Arguments.of("out of bound end", "123456789abcdef0", 7, 10, true),
-        Arguments.of("out of bound start", "123456789abcdef0", 17, 0, true),
-        Arguments.of("invalid minus one", "-1", 0, 1, true),
-        Arguments.of("invalid minus a", "-a", 0, 1, true),
-        Arguments.of("invalid character", "123abcg", 0, 7, true),
-        Arguments.of("invalid upper case A", "A", 0, 1, true),
-        Arguments.of("invalid upper case ABC", "123ABC", 0, 6, true),
-        Arguments.of("too long", repeat("1", 33), 0, 33, true));
-  }
-
   @Test
   void checkZeroConstantInitialization() {
     DDTraceId zero = DDTraceId.ZERO;
diff --git a/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java b/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java
index 2177dcef1d5..5a094eb70ac 100644
--- a/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java
+++ b/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java
@@ -12,20 +12,17 @@
 import java.util.Set;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.tabletest.junit.TableTest;
 
 class IdGenerationStrategyTest {
 
-  @ParameterizedTest(name = "generate id with {1} and {0} bits")
-  @CsvSource({
-    "false,RANDOM",
-    "false,SEQUENTIAL",
-    "false,SECURE_RANDOM",
-    "true,RANDOM",
-    "true,SEQUENTIAL",
-    "true,SECURE_RANDOM"
+  @TableTest({
+    "scenario                 | traceId128BitGenerationEnabled | strategyName ",
+    "strategies-64-bit        | false                          | {RANDOM, SEQUENTIAL, SECURE_RANDOM}",
+    "strategies-128-bit       | true                           | {RANDOM, SEQUENTIAL, SECURE_RANDOM}"
   })
+  @ParameterizedTest(name = "generate id with {1} and {0} bits")
   void generateIdWithStrategyAndBitSize(
       boolean traceId128BitGenerationEnabled, String strategyName) {
     IdGenerationStrategy strategy =
diff --git a/dd-trace-api/src/test/java/datadog/trace/api/internal/util/HexStringUtilsTest.java b/dd-trace-api/src/test/java/datadog/trace/api/internal/util/HexStringUtilsTest.java
index 53fd9ac485e..ec1fefe19fb 100644
--- a/dd-trace-api/src/test/java/datadog/trace/api/internal/util/HexStringUtilsTest.java
+++ b/dd-trace-api/src/test/java/datadog/trace/api/internal/util/HexStringUtilsTest.java
@@ -3,28 +3,17 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.CsvSource;
+import org.tabletest.junit.TableTest;
 
 class HexStringUtilsTest {
 
-  @ParameterizedTest(name = "test hexadecimal String representations high={0} low={1} size={2}")
-  @CsvSource({
-    "0,0,10",
-    "0,0,16",
-    "0,0,20",
-    "0,0,32",
-    "0,0,40",
-    "1,2,10",
-    "1,2,16",
-    "1,2,20",
-    "1,2,32",
-    "1,2,40",
-    "6536977903480360123,3270264562721133536,10",
-    "6536977903480360123,3270264562721133536,16",
-    "6536977903480360123,3270264562721133536,20",
-    "6536977903480360123,3270264562721133536,32",
-    "6536977903480360123,3270264562721133536,40"
+  @TableTest({
+    "scenario      | highOrderBits       | lowOrderBits        | size",
+    "zero          | 0                   | 0                   | {10, 16, 20, 32, 40}",
+    "one-two       | 1                   | 2                   | {10, 16, 20, 32, 40}",
+    "large         | 6536977903480360123 | 3270264562721133536 | {10, 16, 20, 32, 40}"
   })
+  @ParameterizedTest(name = "test hexadecimal String representations high={0} low={1} size={2}")
   void testHexadecimalStringRepresentations(long highOrderBits, long lowOrderBits, int size) {
     int highOrderSize = Math.min(16, Math.max(0, size - 16));
     int lowOrderSize = Math.min(16, size);
diff --git a/gradle/java_deps.gradle b/gradle/java_deps.gradle
index cb0762bf10c..80c4eda9aa3 100644
--- a/gradle/java_deps.gradle
+++ b/gradle/java_deps.gradle
@@ -3,6 +3,7 @@ apply plugin: 'groovy'
 dependencies {
   testImplementation libs.bundles.spock
   testImplementation libs.bundles.groovy
+  testImplementation libs.tabletest
 
   testImplementation libs.bundles.test.logging
 
diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts
index 2e9f00058c3..1f975c4ad45 100644
--- a/utils/test-utils/build.gradle.kts
+++ b/utils/test-utils/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
   api(group = "commons-fileupload", name = "commons-fileupload", version = "1.5")
 
   compileOnly(libs.junit.jupiter)
+  compileOnly(libs.tabletest)
   compileOnly(libs.logback.core)
   compileOnly(libs.logback.classic)
 
diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/TableTestTypeConverters.java b/utils/test-utils/src/main/java/datadog/trace/test/util/TableTestTypeConverters.java
new file mode 100644
index 00000000000..973408cd3fa
--- /dev/null
+++ b/utils/test-utils/src/main/java/datadog/trace/test/util/TableTestTypeConverters.java
@@ -0,0 +1,25 @@
+package datadog.trace.test.util;
+
+import org.tabletest.junit.TypeConverter;
+
+/** Shared converters for JUnit 5 TableTest tests that use unparsable constants. */
+public final class TableTestTypeConverters {
+
+  private TableTestTypeConverters() {}
+
+  @TypeConverter
+  public static long toLong(String value) {
+    if (value == null) {
+      throw new IllegalArgumentException("Value cannot be null");
+    }
+    String token = value.trim();
+    switch (token) {
+      case "Long.MAX_VALUE":
+        return Long.MAX_VALUE;
+      case "Long.MIN_VALUE":
+        return Long.MIN_VALUE;
+      default:
+        return Long.decode(token);
+    }
+  }
+}
