diff --git a/clients/algoliasearch-client-python/algoliasearch/http/api_response.py b/clients/algoliasearch-client-python/algoliasearch/http/api_response.py
index aa7c7de7bb2..ed19413aa69 100644
--- a/clients/algoliasearch-client-python/algoliasearch/http/api_response.py
+++ b/clients/algoliasearch-client-python/algoliasearch/http/api_response.py
@@ -65,9 +65,14 @@ def deserialize(klass: Any = None, data: Any = None) -> Any:
if hasattr(klass, "__origin__") and klass.__origin__ is list:
sub_kls = klass.__args__[0]
- arr = json.loads(data)
+ arr = json.loads(data) if isinstance(data, str) else data
return [ApiResponse.deserialize(sub_kls, sub_data) for sub_data in arr]
+ if hasattr(klass, "__origin__") and klass.__origin__ is dict:
+ sub_kls = klass.__args__[1]
+ obj = json.loads(data) if isinstance(data, str) else data
+ return {k: ApiResponse.deserialize(sub_kls, v) for k, v in obj.items()}
+
if isinstance(klass, str):
if klass.startswith("List["):
sub_kls = match(r"List\[(.*)]", klass)
diff --git a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala
index 7236f1179aa..93e4c045368 100644
--- a/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala
+++ b/clients/algoliasearch-client-scala/src/main/scala/algoliasearch/config/HttpRequest.scala
@@ -100,6 +100,13 @@ object HttpRequest {
this
}
+ def withHeader(key: String, value: Option[Any]): HttpRequest.Builder = {
+ value match {
+ case Some(param) => withHeader(key, param)
+ case None => this
+ }
+ }
+
def withHeaders(headers: Map[String, Any]): HttpRequest.Builder = {
for ((key, value) <- headers)
withHeader(key, value)
diff --git a/config/generation.config.mjs b/config/generation.config.mjs
index c2f950db91e..beee2e7c9f0 100644
--- a/config/generation.config.mjs
+++ b/config/generation.config.mjs
@@ -28,6 +28,7 @@ export const patterns = [
'tests/output/csharp/src/Algolia.Search.Tests.csproj',
'!tests/output/csharp/src/TimeoutIntegrationTests.cs',
+ '!tests/output/csharp/src/Utils/**',
// Dart
'!clients/algoliasearch-client-dart/**',
@@ -64,6 +65,7 @@ export const patterns = [
'tests/output/java/build.gradle',
'!tests/output/java/src/test/java/com/algolia/manual/**',
+ '!tests/output/java/src/test/java/com/algolia/utils/**',
// JavaScript
'!clients/algoliasearch-client-javascript/*',
@@ -92,6 +94,8 @@ export const patterns = [
'clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/api/**',
'clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/model/**',
+ '!tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt',
+
// PHP
'!clients/algoliasearch-client-php/**',
'clients/algoliasearch-client-php/lib/Api/*',
diff --git a/templates/csharp/tests/e2e/e2e.mustache b/templates/csharp/tests/e2e/e2e.mustache
index 1b443e5a7ca..f1638ddd362 100644
--- a/templates/csharp/tests/e2e/e2e.mustache
+++ b/templates/csharp/tests/e2e/e2e.mustache
@@ -6,7 +6,6 @@ using Algolia.Search.Serializer;
using Algolia.Search.Tests.Utils;
using Xunit;
using System.Text.Json;
-using Quibble.Xunit;
using dotenv.net;
{{#isSearchClient}}
using Action = Algolia.Search.Models.Search.Action;
@@ -50,6 +49,7 @@ public class {{client}}RequestTestsE2E
}
+
{{#blocksE2E}}
{{#tests}}
[Fact(DisplayName = "{{{testName}}}")]
@@ -64,7 +64,7 @@ public class {{client}}RequestTestsE2E
{{/statusCode}}
{{#body}}
- JsonAssert.EqualOverrideDefault("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonSerializer.Serialize(resp, JsonConfig.Options), new JsonDiffConfig(true));
+ TestHelpers.LenientJsonAssert("{{#lambda.escapeQuotes}}{{{.}}}{{/lambda.escapeQuotes}}", JsonSerializer.Serialize(resp, JsonConfig.Options));
{{/body}}
} catch (Exception e)
{
diff --git a/templates/go/api.mustache b/templates/go/api.mustache
index 6c6371a4cb2..4fe7dd0259c 100644
--- a/templates/go/api.mustache
+++ b/templates/go/api.mustache
@@ -386,11 +386,11 @@ func (c *APIClient) NewApi{{{nickname}}}Request({{#requiredParams}} {{paramName}
{{#allParams}}
{{^required}}
-// With{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}} adds the {{paramName}} to the {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request and returns the request for chaining.
+// With{{#nameInPascalCase}}{{nameInPascalCase}}{{/nameInPascalCase}}{{^nameInPascalCase}}{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}{{/nameInPascalCase}} adds the {{paramName}} to the {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request and returns the request for chaining.
{{#isDeprecated}}
// Deprecated
{{/isDeprecated}}
-func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request) With{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}({{paramName}} {{^isFreeFormObject}}{{^isArray}}{{^isMap}}{{^isPrimitiveType}}{{^isEnumRef}}*{{/isEnumRef}}{{/isPrimitiveType}}{{/isMap}}{{/isArray}}{{/isFreeFormObject}}{{{dataType}}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request {
+func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request) With{{#nameInPascalCase}}{{nameInPascalCase}}{{/nameInPascalCase}}{{^nameInPascalCase}}{{#lambda.titlecase}}{{baseName}}{{/lambda.titlecase}}{{/nameInPascalCase}}({{paramName}} {{^isFreeFormObject}}{{^isArray}}{{^isMap}}{{^isPrimitiveType}}{{^isEnumRef}}*{{/isEnumRef}}{{/isPrimitiveType}}{{/isMap}}{{/isArray}}{{/isFreeFormObject}}{{{dataType}}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}{{^structPrefix}}Api{{/structPrefix}}{{operationId}}Request {
r.{{paramName}} = {{#isPrimitiveType}}{{^isMap}}&{{/isMap}}{{/isPrimitiveType}}{{paramName}}
return r
}
diff --git a/templates/go/search_helpers.mustache b/templates/go/search_helpers.mustache
index a297bec029f..67b2618cee3 100644
--- a/templates/go/search_helpers.mustache
+++ b/templates/go/search_helpers.mustache
@@ -802,7 +802,13 @@ func (c *APIClient) SaveObjectsWithTransformation(indexName string, objects []ma
return nil, reportError("`region` must be provided at client instantiation before calling this method.")
}
- return c.ingestionTransporter.ChunkedPush(indexName, objects, ingestion.Action(ACTION_ADD_OBJECT), nil, toIngestionChunkedBatchOptions(opts)...) //nolint:wrapcheck
+ //nolint:wrapcheck
+ return c.ingestionTransporter.ChunkedPush(
+ indexName,
+ objects,
+ ingestion.Action(ACTION_ADD_OBJECT),
+ nil,
+ toIngestionChunkedBatchOptions(opts)...)
}
/*
@@ -836,5 +842,11 @@ func (c *APIClient) PartialUpdateObjectsWithTransformation(indexName string, obj
action = ACTION_PARTIAL_UPDATE_OBJECT_NO_CREATE
}
- return c.ingestionTransporter.ChunkedPush(indexName, objects, ingestion.Action(action), nil, toIngestionChunkedBatchOptions(partialUpdateObjectsToChunkedBatchOptions(opts))...) //nolint:wrapcheck
+ //nolint:wrapcheck
+ return c.ingestionTransporter.ChunkedPush(
+ indexName,
+ objects,
+ ingestion.Action(action),
+ nil,
+ toIngestionChunkedBatchOptions(partialUpdateObjectsToChunkedBatchOptions(opts))...)
}
\ No newline at end of file
diff --git a/templates/go/tests/method.mustache b/templates/go/tests/method.mustache
index d2819805aef..2231a58f529 100644
--- a/templates/go/tests/method.mustache
+++ b/templates/go/tests/method.mustache
@@ -1,2 +1,2 @@
client.{{#lambda.titlecase}}{{method}}{{/lambda.titlecase}}({{#hasParams}}{{^isHelper}}client.NewApi{{#lambda.titlecase}}{{method}}{{/lambda.titlecase}}Request({{/isHelper}}
- {{#parametersWithDataType}}{{#required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}} {{^isHelper}}){{#parametersWithDataType}}{{^required}}.With{{#lambda.pascalcase}}{{{key}}}{{/lambda.pascalcase}}({{> tests/generateParams}}){{#-last}},{{/-last}}{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{#isHelper}}{{#parametersWithDataType}}{{^required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{/hasParams}}{{#requestOptions}}{{#queryParameters.parametersWithDataType}}{{clientPrefix}}.WithQueryParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/queryParameters.parametersWithDataType}}{{#headers.parametersWithDataType}}{{clientPrefix}}.WithHeaderParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/headers.parametersWithDataType}} {{#timeouts.read}} ,{{clientPrefix}}.WithReadTimeout({{.}} * time.Millisecond), {{/timeouts.read}} {{#timeouts.write}} ,{{clientPrefix}}.WithWriteTimeout({{.}} * time.Millisecond), {{/timeouts.write}} {{#timeouts.connect}} ,{{clientPrefix}}.WithConnectTimeout({{.}} * time.Millisecond), {{/timeouts.connect}} {{/requestOptions}})
\ No newline at end of file
+ {{#parametersWithDataType}}{{#required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}} {{^isHelper}}){{#parametersWithDataType}}{{^required}}{{^isNull}}.With{{#lambda.pascalcase}}{{{key}}}{{/lambda.pascalcase}}({{> tests/generateParams}}){{/isNull}}{{#-last}},{{/-last}}{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{#isHelper}}{{#parametersWithDataType}}{{^required}}{{> tests/generateParams}},{{/required}}{{/parametersWithDataType}}{{/isHelper}}{{/hasParams}}{{#requestOptions}}{{#queryParameters.parametersWithDataType}}{{clientPrefix}}.WithQueryParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/queryParameters.parametersWithDataType}}{{#headers.parametersWithDataType}}{{clientPrefix}}.WithHeaderParam("{{{key}}}", {{> tests/generateInnerParams}}),{{/headers.parametersWithDataType}} {{#timeouts.read}} ,{{clientPrefix}}.WithReadTimeout({{.}} * time.Millisecond), {{/timeouts.read}} {{#timeouts.write}} ,{{clientPrefix}}.WithWriteTimeout({{.}} * time.Millisecond), {{/timeouts.write}} {{#timeouts.connect}} ,{{clientPrefix}}.WithConnectTimeout({{.}} * time.Millisecond), {{/timeouts.connect}} {{/requestOptions}})
\ No newline at end of file
diff --git a/templates/java/tests/e2e/e2e.mustache b/templates/java/tests/e2e/e2e.mustache
index 3ef00c05013..196647c9e12 100644
--- a/templates/java/tests/e2e/e2e.mustache
+++ b/templates/java/tests/e2e/e2e.mustache
@@ -12,8 +12,7 @@ import io.github.cdimascio.dotenv.Dotenv;
import java.util.*;
import java.time.Duration;
import org.junit.jupiter.api.*;
-import org.skyscreamer.jsonassert.JSONAssert;
-import org.skyscreamer.jsonassert.JSONCompareMode;
+import com.algolia.utils.TestHelpers;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class {{client}}RequestsTestsE2E {
@@ -45,7 +44,7 @@ class {{client}}RequestsTestsE2E {
{{returnType}} res = {{> tests/method}};
{{#response}}
{{#body}}
- assertDoesNotThrow(() -> JSONAssert.assertEquals("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", json.writeValueAsString(res), JSONCompareMode.LENIENT));
+ assertDoesNotThrow(() -> TestHelpers.lenientJsonAssert("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", json.writeValueAsString(res)));
{{/body}}
{{/response}}
}
diff --git a/templates/kotlin/builder_function.mustache b/templates/kotlin/builder_function.mustache
index 67056984cd9..c5edebc751f 100644
--- a/templates/kotlin/builder_function.mustache
+++ b/templates/kotlin/builder_function.mustache
@@ -8,7 +8,7 @@
*/
public fun {{vendorExtensions.x-one-of-explicit-name}}(
{{#vars}}
- {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}},
+ {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}},
{{/vars}}
): {{classname}} = {{vendorExtensions.x-fully-qualified-classname}} (
{{#vars}}
diff --git a/templates/kotlin/data_class_field.mustache b/templates/kotlin/data_class_field.mustache
index 5a4a65f2f6a..70336d084fc 100644
--- a/templates/kotlin/data_class_field.mustache
+++ b/templates/kotlin/data_class_field.mustache
@@ -4,4 +4,4 @@
{{#deprecated}}
@Deprecated(message = "This property is deprecated.")
{{/deprecated}}
- @SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}}
+ @SerialName(value = "{{{vendorExtensions.x-base-name-literal}}}") {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}}
diff --git a/templates/kotlin/json_object_field.mustache b/templates/kotlin/json_object_field.mustache
index fcced575c14..99d08309c14 100644
--- a/templates/kotlin/json_object_field.mustache
+++ b/templates/kotlin/json_object_field.mustache
@@ -4,4 +4,4 @@
{{#deprecated}}
@Deprecated(message = "This property is deprecated.")
{{/deprecated}}
- {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{^required}}? = null{{/required}}
+ {{#isInherited}}override {{/isInherited}}val {{{name}}}: {{> data_class_field_type}}{{#required}}{{#isNullable}}? = null{{/isNullable}}{{/required}}{{^required}}? = null{{/required}}
diff --git a/templates/kotlin/tests/e2e/e2e.mustache b/templates/kotlin/tests/e2e/e2e.mustache
index 1d02f27f596..1d95fbf0ac2 100644
--- a/templates/kotlin/tests/e2e/e2e.mustache
+++ b/templates/kotlin/tests/e2e/e2e.mustache
@@ -6,8 +6,6 @@ import com.algolia.client.model.{{import}}.*
import com.algolia.client.configuration.*
import com.algolia.client.transport.*
import com.algolia.utils.*
-import org.skyscreamer.jsonassert.JSONAssert;
-import org.skyscreamer.jsonassert.JSONCompareMode;
import io.ktor.http.*
import io.github.cdimascio.dotenv.Dotenv
import kotlinx.coroutines.test.*
@@ -44,7 +42,7 @@ class {{clientPrefix}}Test {
{{#response}}
{{#body}}
response = {
- JSONAssert.assertEquals("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", Json.encodeToString(it), JSONCompareMode.LENIENT)
+ lenientJsonAssert("{{#lambda.escapeQuotes}}{{{body}}}{{/lambda.escapeQuotes}}", Json.encodeToString(it))
},
{{/body}}
{{/response}}
diff --git a/templates/kotlin/tests/request_param.mustache b/templates/kotlin/tests/request_param.mustache
index 0146bf2f787..de0f8367fa4 100644
--- a/templates/kotlin/tests/request_param.mustache
+++ b/templates/kotlin/tests/request_param.mustache
@@ -1 +1 @@
-{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}
\ No newline at end of file
+{{^isNull}}{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}{{/isNull}}{{#isNull}}{{#required}}{{^hasAdditionalProperties}}{{#lambda.camelcase}}{{{key}}}{{/lambda.camelcase}} = {{> tests/param_value}},{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}},{{/hasAdditionalProperties}}{{/required}}{{/isNull}}
\ No newline at end of file
diff --git a/templates/scala/tests/request_param.mustache b/templates/scala/tests/request_param.mustache
index 0c5bacece1f..31030d474f4 100644
--- a/templates/scala/tests/request_param.mustache
+++ b/templates/scala/tests/request_param.mustache
@@ -1 +1 @@
-{{^hasAdditionalProperties}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}}{{/hasAdditionalProperties}}
\ No newline at end of file
+{{^hasAdditionalProperties}}{{#isNull}}{{^required}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = None{{/required}}{{#required}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/required}}{{/isNull}}{{^isNull}}{{#lambda.identifier}}{{key}}{{/lambda.identifier}} = {{> tests/param_optional}}{{/isNull}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}{{> tests/param_value}}{{/hasAdditionalProperties}}
\ No newline at end of file
diff --git a/templates/swift/tests/paramValue.mustache b/templates/swift/tests/paramValue.mustache
index 90462b8d176..27fe45cdaa4 100644
--- a/templates/swift/tests/paramValue.mustache
+++ b/templates/swift/tests/paramValue.mustache
@@ -1 +1 @@
-{{#isVerbatim}}{{{value}}}{{/isVerbatim}}{{#isObject}}{{objectName}}({{^hasAdditionalProperties}}{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}from: [{{#value}}"{{key}}": AnyCodable({{> tests/paramValue }}){{^-last}}, {{/-last}}{{/value}}]{{/hasAdditionalProperties}}){{/isObject}}{{#isString}}{{#isAnyType}}AnyCodable({{/isAnyType}}"{{#lambda.escapeQuotes}}{{{value}}}{{/lambda.escapeQuotes}}"{{#isAnyType}}){{/isAnyType}}{{/isString}}{{#isNumber}}{{#isLong}}Int64({{/isLong}}{{{value}}}{{#isLong}}){{/isLong}}{{/isNumber}}{{#isBoolean}}{{{value}}}{{/isBoolean}}{{#isEnum}}{{objectName}}.{{#lambda.identifier}}{{#lambda.camelcase}}{{value}}{{/lambda.camelcase}}{{/lambda.identifier}}{{/isEnum}}{{#isArray}}[{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}]{{/isArray}}{{#isMap}}Map[{{{.}}}]{{/isMap}}{{#isFreeFormObject}}{{#isAnyType}}[{{#value}}{{#entrySet}}"{{key}}": "{{{value}}}"{{/entrySet}}{{/value}}]{{/isAnyType}}{{^isAnyType}}{{^value}}[String: AnyCodable](){{/value}}{{#value}}{{#-first}}[{{/-first}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{#-last}}]{{/-last}}{{/value}}{{/isAnyType}}{{/isFreeFormObject}}{{#isNull}}{{#inClientTest}}TestNull{{{objectName}}}(){{/inClientTest}}{{/isNull}}
\ No newline at end of file
+{{#isVerbatim}}{{{value}}}{{/isVerbatim}}{{#isObject}}{{objectName}}({{^hasAdditionalProperties}}{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}{{/hasAdditionalProperties}}{{#hasAdditionalProperties}}from: [{{#value}}"{{key}}": AnyCodable({{> tests/paramValue }}){{^-last}}, {{/-last}}{{/value}}]{{/hasAdditionalProperties}}){{/isObject}}{{#isString}}{{#isAnyType}}AnyCodable({{/isAnyType}}"{{#lambda.escapeQuotes}}{{{value}}}{{/lambda.escapeQuotes}}"{{#isAnyType}}){{/isAnyType}}{{/isString}}{{#isNumber}}{{#isLong}}Int64({{/isLong}}{{{value}}}{{#isLong}}){{/isLong}}{{/isNumber}}{{#isBoolean}}{{{value}}}{{/isBoolean}}{{#isEnum}}{{objectName}}.{{#lambda.identifier}}{{#lambda.camelcase}}{{value}}{{/lambda.camelcase}}{{/lambda.identifier}}{{/isEnum}}{{#isArray}}[{{#value}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{/value}}]{{/isArray}}{{#isMap}}Map[{{{.}}}]{{/isMap}}{{#isFreeFormObject}}{{#isAnyType}}[{{#value}}{{#entrySet}}"{{key}}": "{{{value}}}"{{/entrySet}}{{/value}}]{{/isAnyType}}{{^isAnyType}}{{^value}}[String: AnyCodable](){{/value}}{{#value}}{{#-first}}[{{/-first}}{{> tests/generateParams}}{{^-last}}, {{/-last}}{{#-last}}]{{/-last}}{{/value}}{{/isAnyType}}{{/isFreeFormObject}}{{#isNull}}{{#inClientTest}}TestNull{{{objectName}}}(){{/inClientTest}}{{^inClientTest}}nil{{/inClientTest}}{{/isNull}}
\ No newline at end of file
diff --git a/tests/output/csharp/src/Utils/TestHelpers.cs b/tests/output/csharp/src/Utils/TestHelpers.cs
new file mode 100644
index 00000000000..7264da6a005
--- /dev/null
+++ b/tests/output/csharp/src/Utils/TestHelpers.cs
@@ -0,0 +1,54 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Quibble.Xunit;
+
+namespace Algolia.Search.Tests.Utils;
+
+public static class TestHelpers
+{
+ ///
+ /// Asserts that the serialized response contains at least the expected JSON structure.
+ /// Extra keys in objects and extra elements in arrays (beyond expected indices) are ignored.
+ /// Mirrors the union-based e2e assertion used by JS, Python, Ruby, PHP, Go, and Swift clients.
+ ///
+ public static void LenientJsonAssert(string expected, string actual)
+ {
+ var expectedNode = JsonNode.Parse(expected);
+ var actualNode = JsonNode.Parse(actual);
+ var unionNode = Union(expectedNode, actualNode);
+ var unionJson = JsonSerializer.Serialize(unionNode);
+ JsonAssert.EqualOverrideDefault(expected, unionJson, new JsonDiffConfig(true));
+ }
+
+ ///
+ /// Recursively intersects the structure of with the values of
+ /// . Only keys/indices present in expected are kept.
+ ///
+ private static JsonNode Union(JsonNode expected, JsonNode received)
+ {
+ if (expected is JsonObject expectedObj && received is JsonObject receivedObj)
+ {
+ var result = new JsonObject();
+ foreach (var prop in expectedObj)
+ {
+ if (receivedObj[prop.Key] != null)
+ {
+ result[prop.Key] = Union(prop.Value, receivedObj[prop.Key]);
+ }
+ }
+ return result;
+ }
+
+ if (expected is JsonArray expectedArr && received is JsonArray receivedArr)
+ {
+ var result = new JsonArray();
+ for (int i = 0; i < expectedArr.Count && i < receivedArr.Count; i++)
+ {
+ result.Add(Union(expectedArr[i], receivedArr[i]));
+ }
+ return result;
+ }
+
+ return received.DeepClone();
+ }
+}
diff --git a/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java b/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java
new file mode 100644
index 00000000000..9b214fde036
--- /dev/null
+++ b/tests/output/java/src/test/java/com/algolia/utils/TestHelpers.java
@@ -0,0 +1,54 @@
+package com.algolia.utils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+public class TestHelpers {
+
+ private static final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * Asserts that the serialized response contains at least the expected JSON structure. Extra keys
+ * in objects and extra elements in arrays (beyond expected indices) are ignored. Mirrors the
+ * union-based e2e assertion used by JS, Python, Ruby, PHP, Go, Swift, and C# clients.
+ */
+ public static void lenientJsonAssert(String expected, String actual) throws Exception {
+ JsonNode expectedNode = mapper.readTree(expected);
+ JsonNode actualNode = mapper.readTree(actual);
+ JsonNode unionNode = union(expectedNode, actualNode);
+ String unionJson = mapper.writeValueAsString(unionNode);
+ JSONAssert.assertEquals(expected, unionJson, JSONCompareMode.LENIENT);
+ }
+
+ /**
+ * Recursively intersects the structure of {@code expected} with the values of {@code received}.
+ * Only keys/indices present in expected are kept.
+ */
+ private static JsonNode union(JsonNode expected, JsonNode received) {
+ if (expected.isObject() && received.isObject()) {
+ ObjectNode result = mapper.createObjectNode();
+ expected
+ .fieldNames()
+ .forEachRemaining(key -> {
+ if (received.has(key)) {
+ result.set(key, union(expected.get(key), received.get(key)));
+ }
+ });
+ return result;
+ }
+
+ if (expected.isArray() && received.isArray()) {
+ ArrayNode result = mapper.createArrayNode();
+ for (int i = 0; i < expected.size() && i < received.size(); i++) {
+ result.add(union(expected.get(i), received.get(i)));
+ }
+ return result;
+ }
+
+ return received.deepCopy();
+ }
+}
diff --git a/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt
new file mode 100644
index 00000000000..47ddbf160bf
--- /dev/null
+++ b/tests/output/kotlin/src/commonTest/kotlin/com/algolia/utils/TestHelpers.kt
@@ -0,0 +1,46 @@
+package com.algolia.utils
+
+import kotlinx.serialization.json.*
+import org.skyscreamer.jsonassert.JSONAssert
+import org.skyscreamer.jsonassert.JSONCompareMode
+
+/**
+ * Asserts that the serialized response contains at least the expected JSON structure. Extra keys in
+ * objects and extra elements in arrays (beyond expected indices) are ignored. Mirrors the
+ * union-based e2e assertion used by JS, Python, Ruby, PHP, Go, Swift, and C# clients.
+ */
+public fun lenientJsonAssert(expected: String, actual: String) {
+ val expectedNode = Json.parseToJsonElement(expected)
+ val actualNode = Json.parseToJsonElement(actual)
+ val unionNode = union(expectedNode, actualNode)
+ val unionJson = Json.encodeToString(JsonElement.serializer(), unionNode)
+ JSONAssert.assertEquals(expected, unionJson, JSONCompareMode.LENIENT)
+}
+
+/**
+ * Recursively intersects the structure of [expected] with the values of [received]. Only
+ * keys/indices present in expected are kept.
+ */
+private fun union(expected: JsonElement, received: JsonElement): JsonElement {
+ if (expected is JsonObject && received is JsonObject) {
+ return buildJsonObject {
+ for ((key, value) in expected) {
+ if (key in received) {
+ put(key, union(value, received[key]!!))
+ }
+ }
+ }
+ }
+
+ if (expected is JsonArray && received is JsonArray) {
+ return buildJsonArray {
+ for (i in expected.indices) {
+ if (i < received.size) {
+ add(union(expected[i], received[i]))
+ }
+ }
+ }
+ }
+
+ return received
+}