From 60f1e7048112871fdb37bf0193e2b823cec835bb Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Mon, 17 Nov 2025 10:55:58 +0100 Subject: [PATCH 01/11] Delegate parsing and writing of value types to Gson This allows registering custom TypeAdapters to customize (un)marshalling of these value types. --- .../serialization/DefaultGsonBuilder.java | 132 ++++++++++++++++++ .../kiota/serialization/JsonParseNode.java | 65 ++++----- .../serialization/JsonParseNodeFactory.java | 20 ++- .../JsonSerializationWriter.java | 29 +++- .../JsonSerializationWriterFactory.java | 21 ++- .../serialization/JsonParseNodeTests.java | 75 +++++++++- .../JsonSerializationWriterTests.java | 26 +++- 7 files changed, 319 insertions(+), 49 deletions(-) create mode 100644 components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java new file mode 100644 index 000000000..cd70927aa --- /dev/null +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -0,0 +1,132 @@ +package com.microsoft.kiota.serialization; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.microsoft.kiota.PeriodAndDuration; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; + +public class DefaultGsonBuilder { + + private static final TypeAdapter OFFSET_DATE_TIME = + new TypeAdapter<>() { + @Override + public OffsetDateTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return OffsetDateTime.parse(stringValue); + } catch (DateTimeParseException ex) { + // Append UTC offset if it's missing + try { + LocalDateTime localDateTime = LocalDateTime.parse(stringValue); + return localDateTime.atOffset(ZoneOffset.UTC); + } catch (DateTimeParseException ex2) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as OffsetDateTime; at path " + + in.getPreviousPath(), + ex2); + } + } + } + + @Override + public void write(JsonWriter out, OffsetDateTime value) throws IOException { + out.value(value.toString()); + } + }; + + private static final TypeAdapter LOCAL_DATE = + new TypeAdapter<>() { + @Override + public LocalDate read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return LocalDate.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalDate; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, LocalDate value) throws IOException { + out.value(value.toString()); + } + }; + + private static final TypeAdapter LOCAL_TIME = + new TypeAdapter<>() { + @Override + public LocalTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return LocalTime.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalTime; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, LocalTime value) throws IOException { + out.value(value.toString()); + } + }; + + private static final TypeAdapter PERIOD_AND_DURATION = + new TypeAdapter<>() { + @Override + public PeriodAndDuration read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return PeriodAndDuration.parse(stringValue); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as PeriodAndDuration; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, PeriodAndDuration value) throws IOException { + out.value(value.toString()); + } + }; + + private static final Gson defaultInstance = getDefaultBuilder().create(); + + public static Gson getDefaultInstance() { + return defaultInstance; + } + + public static GsonBuilder getDefaultBuilder() { + return new GsonBuilder() + .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME.nullSafe()) + .registerTypeAdapter(LocalDate.class, LOCAL_DATE.nullSafe()) + .registerTypeAdapter(LocalTime.class, LOCAL_TIME.nullSafe()) + .registerTypeAdapter(PeriodAndDuration.class, PERIOD_AND_DURATION.nullSafe()); + } +} diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index 8ee6e59a5..c5bd377d7 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -11,11 +12,8 @@ import java.math.BigDecimal; import java.time.LocalDate; -import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Base64; import java.util.EnumSet; @@ -31,13 +29,36 @@ /** ParseNode implementation for JSON */ public class JsonParseNode implements ParseNode { private final JsonElement currentNode; + protected final Gson gson; /** * Creates a new instance of the JsonParseNode class. * @param node the node to wrap. + * @deprecated use {@link #JsonParseNode(JsonElement, Gson)} instead. */ + @Deprecated public JsonParseNode(@Nonnull final JsonElement node) { currentNode = Objects.requireNonNull(node, "parameter node cannot be null"); + this.gson = DefaultGsonBuilder.getDefaultInstance(); + } + + /** + * Creates a new instance of the JsonParseNode class. + * @param node the node to wrap. + * @param node the node to wrap. + */ + public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) { + currentNode = Objects.requireNonNull(node, "parameter node cannot be null"); + this.gson = Objects.requireNonNull(gson, "parameter gson cannot be null"); + } + + /** + * Creates a new {@link JsonParseNode} for the given {@link JsonElement}. + * @param node the node to wrap. + * @return the newly created {@link JsonParseNode}. + */ + @Nonnull protected JsonParseNode createNewNode(@Nonnull JsonElement node) { + return new JsonParseNode(node, gson); } /** {@inheritDoc} */ @@ -47,7 +68,7 @@ public JsonParseNode(@Nonnull final JsonElement node) { final JsonObject object = currentNode.getAsJsonObject(); final JsonElement childNodeElement = object.get(identifier); if (childNodeElement == null) return null; - final JsonParseNode result = new JsonParseNode(childNodeElement); + final JsonParseNode result = createNewNode(childNodeElement); result.setOnBeforeAssignFieldValues(this.onBeforeAssignFieldValues); result.setOnAfterAssignFieldValues(this.onAfterAssignFieldValues); return result; @@ -91,43 +112,23 @@ public JsonParseNode(@Nonnull final JsonElement node) { } @Nullable public UUID getUUIDValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return UUID.fromString(stringValue); + return gson.fromJson(currentNode, UUID.class); } @Nullable public OffsetDateTime getOffsetDateTimeValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - try { - return OffsetDateTime.parse(stringValue); - } catch (DateTimeParseException ex) { - // Append UTC offset if it's missing - try { - LocalDateTime localDateTime = LocalDateTime.parse(stringValue); - return localDateTime.atOffset(ZoneOffset.UTC); - } catch (DateTimeParseException ex2) { - throw ex; - } - } + return gson.fromJson(currentNode, OffsetDateTime.class); } @Nullable public LocalDate getLocalDateValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return LocalDate.parse(stringValue); + return gson.fromJson(currentNode, LocalDate.class); } @Nullable public LocalTime getLocalTimeValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return LocalTime.parse(stringValue); + return gson.fromJson(currentNode, LocalTime.class); } @Nullable public PeriodAndDuration getPeriodAndDurationValue() { - final String stringValue = currentNode.getAsString(); - if (stringValue == null) return null; - return PeriodAndDuration.parse(stringValue); + return gson.fromJson(currentNode, PeriodAndDuration.class); } @Nullable private T getPrimitiveValue( @@ -171,7 +172,7 @@ private List iterateOnArray(JsonElement jsonElement, Function result = new ArrayList<>(); while (sourceIterator.hasNext()) { final JsonElement item = sourceIterator.next(); - final JsonParseNode itemNode = new JsonParseNode(item); + final JsonParseNode itemNode = createNewNode(item); itemNode.setOnBeforeAssignFieldValues(this.getOnBeforeAssignFieldValues()); itemNode.setOnAfterAssignFieldValues(this.getOnAfterAssignFieldValues()); result.add(fn.apply(itemNode)); @@ -237,7 +238,7 @@ else if (element.isJsonPrimitive()) { element.getAsJsonObject().entrySet()) { final String fieldKey = fieldEntry.getKey(); final JsonElement fieldValue = fieldEntry.getValue(); - final JsonParseNode childNode = new JsonParseNode(fieldValue); + final JsonParseNode childNode = createNewNode(fieldValue); childNode.setOnBeforeAssignFieldValues(this.getOnBeforeAssignFieldValues()); childNode.setOnAfterAssignFieldValues(this.getOnAfterAssignFieldValues()); propertiesMap.put(fieldKey, childNode.getUntypedValue()); @@ -294,7 +295,7 @@ private void assignFieldValues( final JsonElement fieldValue = fieldEntry.getValue(); if (fieldValue.isJsonNull()) continue; if (fieldDeserializer != null) { - final JsonParseNode itemNode = new JsonParseNode(fieldValue); + final JsonParseNode itemNode = createNewNode(fieldValue); itemNode.setOnBeforeAssignFieldValues(this.onBeforeAssignFieldValues); itemNode.setOnAfterAssignFieldValues(this.onAfterAssignFieldValues); fieldDeserializer.accept(itemNode); diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java index c0c68eaae..4c0642871 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.JsonParser; import jakarta.annotation.Nonnull; @@ -22,6 +23,23 @@ public JsonParseNodeFactory() {} private static final String validContentType = "application/json"; + private Gson gson = DefaultGsonBuilder.getDefaultInstance(); + + /** + * @return the {@link Gson} instance to use for parsing value types. + */ + @Nonnull public Gson getGson() { + return gson; + } + + /** + * Specify a custom {@link Gson} instance for parsing value types. + * @param gson the {@link Gson} instance to use. + */ + public void setGson(@Nonnull Gson gson) { + this.gson = gson; + } + /** {@inheritDoc} */ @Override @Nonnull public ParseNode getParseNode( @@ -35,7 +53,7 @@ public JsonParseNodeFactory() {} } try (final InputStreamReader reader = new InputStreamReader(rawResponse, StandardCharsets.UTF_8)) { - return new JsonParseNode(JsonParser.parseReader(reader)); + return new JsonParseNode(JsonParser.parseReader(reader), gson); } catch (IOException ex) { throw new RuntimeException("could not close the reader", ex); } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java index f050d9965..0443d697c 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; import com.google.gson.stream.JsonWriter; import com.microsoft.kiota.PeriodAndDuration; @@ -17,7 +18,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; import java.util.Base64; import java.util.EnumSet; import java.util.List; @@ -34,10 +34,25 @@ public class JsonSerializationWriter implements SerializationWriter { private final ByteArrayOutputStream stream = new ByteArrayOutputStream(); private final JsonWriter writer; + private final Gson gson; - /** Creates a new instance of a json serialization writer */ + /** + * Creates a new instance of a json serialization writer + * @deprecated use {@link #JsonSerializationWriter(Gson)} instead. + */ + @Deprecated public JsonSerializationWriter() { this.writer = new JsonWriter(new OutputStreamWriter(this.stream, StandardCharsets.UTF_8)); + this.gson = DefaultGsonBuilder.getDefaultInstance(); + } + + /** + * Creates a new instance of a json serialization writer + * @param gson the {@link Gson} instance to use for writing value types. + */ + public JsonSerializationWriter(@Nonnull Gson gson) { + this.writer = new JsonWriter(new OutputStreamWriter(this.stream, StandardCharsets.UTF_8)); + this.gson = Objects.requireNonNull(gson, "parameter gson cannot be null"); } public void writeStringValue(@Nullable final String key, @Nullable final String value) { @@ -154,7 +169,7 @@ public void writeUUIDValue(@Nullable final String key, @Nullable final UUID valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.toString()); + gson.getAdapter(UUID.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -167,7 +182,7 @@ public void writeOffsetDateTimeValue( if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + gson.getAdapter(OffsetDateTime.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -179,7 +194,7 @@ public void writeLocalDateValue(@Nullable final String key, @Nullable final Loca if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); + gson.getAdapter(LocalDate.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -191,7 +206,7 @@ public void writeLocalTimeValue(@Nullable final String key, @Nullable final Loca if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.format(DateTimeFormatter.ISO_LOCAL_TIME)); + gson.getAdapter(LocalTime.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -204,7 +219,7 @@ public void writePeriodAndDurationValue( if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value.toString()); + gson.getAdapter(PeriodAndDuration.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java index c72d01d0b..a0fe43bc8 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java @@ -1,5 +1,7 @@ package com.microsoft.kiota.serialization; +import com.google.gson.Gson; + import jakarta.annotation.Nonnull; import java.util.Objects; @@ -16,6 +18,23 @@ public JsonSerializationWriterFactory() {} private static final String validContentType = "application/json"; + private Gson gson = DefaultGsonBuilder.getDefaultInstance(); + + /** + * @return the {@link Gson} instance to use for writing value types. + */ + @Nonnull public Gson getGson() { + return gson; + } + + /** + * Specify a custom {@link Gson} instance for writing value types. + * @param gson the {@link Gson} instance to use. + */ + public void setGson(@Nonnull Gson gson) { + this.gson = gson; + } + /** {@inheritDoc} */ @Override @Nonnull public SerializationWriter getSerializationWriter(@Nonnull final String contentType) { @@ -25,6 +44,6 @@ public JsonSerializationWriterFactory() {} } else if (!contentType.equals(validContentType)) { throw new IllegalArgumentException("expected a " + validContentType + " content type"); } - return new JsonSerializationWriter(); + return new JsonSerializationWriter(gson); } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java index ec31bd0e1..8447f083e 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonParseNodeTests.java @@ -1,8 +1,17 @@ package com.microsoft.kiota.serialization; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.gson.Gson; import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import com.microsoft.kiota.serialization.mocks.MyEnum; import com.microsoft.kiota.serialization.mocks.TestEntity; import com.microsoft.kiota.serialization.mocks.UntypedTestEntity; @@ -12,7 +21,11 @@ import org.junit.jupiter.params.provider.ValueSource; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; class JsonParseNodeTests { @@ -66,6 +79,44 @@ class JsonParseNodeTests { + " }\r\n" + "}"; + public static final DateTimeFormatter customFormatter = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .parseLenient() + .appendOffset("+HHmm", "+0000") + .parseStrict() + .toFormatter(); + + public static final Gson customGson = + DefaultGsonBuilder.getDefaultBuilder() + .registerTypeAdapter( + OffsetDateTime.class, + new TypeAdapter() { + @Override + public OffsetDateTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + return customFormatter.parse( + stringValue, OffsetDateTime::from); + } catch (DateTimeParseException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as LocalDate; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, OffsetDateTime value) + throws IOException { + out.value(customFormatter.format(value)); + } + }.nullSafe()) + .create(); + @Test void itDDoesNotFailForGetChildElementOnMissingKey() throws UnsupportedEncodingException { final var initialString = "{displayName\": \"Microsoft Teams Meeting\"}"; @@ -79,7 +130,9 @@ void itDDoesNotFailForGetChildElementOnMissingKey() throws UnsupportedEncodingEx void testParsesDateTimeOffset() { final var dateTimeOffsetString = "2024-02-12T19:47:39+02:00"; final var jsonElement = JsonParser.parseString("\"" + dateTimeOffsetString + "\""); - final var result = new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + final var result = + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); assertEquals(dateTimeOffsetString, result.toString()); } @@ -87,7 +140,9 @@ void testParsesDateTimeOffset() { void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { final var dateTimeString = "2024-02-12T19:47:39"; final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); - final var result = new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + final var result = + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); assertEquals(dateTimeString + "Z", result.toString()); } @@ -96,13 +151,23 @@ void testParsesDateTimeStringWithoutOffsetToDateTimeOffset() { void testInvalidOffsetDateTimeStringThrowsException(final String dateTimeString) { final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); try { - new JsonParseNode(jsonElement).getOffsetDateTimeValue(); + new JsonParseNode(jsonElement, DefaultGsonBuilder.getDefaultInstance()) + .getOffsetDateTimeValue(); } catch (final Exception ex) { - assertInstanceOf(DateTimeParseException.class, ex); + assertInstanceOf(JsonSyntaxException.class, ex); assertTrue(ex.getMessage().contains(dateTimeString)); } } + @Test + void testNonStandardOffsetDateTimeParsing() { + final var dateTimeString = "2024-02-12T19:47:39+0000"; + final var jsonElement = JsonParser.parseString("\"" + dateTimeString + "\""); + final var parsedOffsetDateTime = + new JsonParseNode(jsonElement, customGson).getOffsetDateTimeValue(); + assertEquals(OffsetDateTime.parse("2024-02-12T19:47:39+00:00"), parsedOffsetDateTime); + } + @Test void getEntityWithArrayInAdditionalData() throws UnsupportedEncodingException { final var rawResponse = new ByteArrayInputStream(testJsonString.getBytes("UTF-8")); diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java index f9a5f5038..5a3834771 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -53,7 +54,8 @@ void writesSampleObjectValueWithPrimitivesInAdditionalData() throws IOException .getAdditionalData() .put("aliases", aliases); // place a collection in the additional data - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", testEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = @@ -66,6 +68,22 @@ void writesSampleObjectValueWithPrimitivesInAdditionalData() throws IOException } } + @Test + void useNonStandardOffsetDateTimeFormat() throws IOException { + var testEntity = new TestEntity(); + testEntity.setCreatedDateTime(OffsetDateTime.parse("2024-02-12T19:47:39+00:00")); + try (final var jsonSerializer = + new JsonSerializationWriter(JsonParseNodeTests.customGson)) { + jsonSerializer.writeObjectValue("", testEntity); + var contentStream = jsonSerializer.getSerializedContent(); + var serializedJsonString = + new String(Compatibility.readAllBytes(contentStream), "UTF-8"); + // Assert + var expectedString = "{\"createdDateTime\":\"2024-02-12T19:47:39+0000\"}"; + assertEquals(expectedString, serializedJsonString); + } + } + @Test void writesSampleObjectValueWithParsableInAdditionalData() throws IOException { var testEntity = new TestEntity(); @@ -81,7 +99,8 @@ void writesSampleObjectValueWithParsableInAdditionalData() throws IOException { .getAdditionalData() .put("manager", managerAdditionalData); // place a parsable in the addtionaldata - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", testEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = @@ -186,7 +205,8 @@ void writesSampleObjectValueWithUntypedProperties() throws IOException { } })); - try (final var jsonSerializer = new JsonSerializationWriter()) { + try (final var jsonSerializer = + new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance())) { jsonSerializer.writeObjectValue("", untypedTestEntity); var contentStream = jsonSerializer.getSerializedContent(); var serializedJsonString = From bb6e01de4f453160db038a8fe946b9b885544871 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Mon, 17 Nov 2025 21:54:17 +0100 Subject: [PATCH 02/11] Use Gson for parsing all types, minor code simplification --- .../kiota/serialization/JsonParseNode.java | 57 ++++--------------- .../serialization/JsonParseNodeFactory.java | 25 ++++---- .../JsonSerializationWriter.java | 5 +- .../JsonSerializationWriterFactory.java | 25 ++++---- 4 files changed, 41 insertions(+), 71 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index c5bd377d7..d375fb1b2 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -28,18 +28,15 @@ /** ParseNode implementation for JSON */ public class JsonParseNode implements ParseNode { - private final JsonElement currentNode; + protected final JsonElement currentNode; protected final Gson gson; /** * Creates a new instance of the JsonParseNode class. * @param node the node to wrap. - * @deprecated use {@link #JsonParseNode(JsonElement, Gson)} instead. */ - @Deprecated public JsonParseNode(@Nonnull final JsonElement node) { - currentNode = Objects.requireNonNull(node, "parameter node cannot be null"); - this.gson = DefaultGsonBuilder.getDefaultInstance(); + this(node, DefaultGsonBuilder.getDefaultInstance()); } /** @@ -76,39 +73,39 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) } @Nullable public String getStringValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsString() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, String.class) : null; } @Nullable public Boolean getBooleanValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsBoolean() : null; + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Boolean.class) : null; } @Nullable public Byte getByteValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsByte() : null; + return gson.fromJson(currentNode, Byte.class); } @Nullable public Short getShortValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsShort() : null; + return gson.fromJson(currentNode, Short.class); } @Nullable public BigDecimal getBigDecimalValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsBigDecimal() : null; + return gson.fromJson(currentNode, BigDecimal.class); } @Nullable public Integer getIntegerValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsInt() : null; + return gson.fromJson(currentNode, Integer.class); } @Nullable public Float getFloatValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsFloat() : null; + return gson.fromJson(currentNode, Float.class); } @Nullable public Double getDoubleValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsDouble() : null; + return gson.fromJson(currentNode, Double.class); } @Nullable public Long getLongValue() { - return currentNode.isJsonPrimitive() ? currentNode.getAsLong() : null; + return gson.fromJson(currentNode, Long.class); } @Nullable public UUID getUUIDValue() { @@ -133,37 +130,7 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) @Nullable private T getPrimitiveValue( @Nonnull final Class targetClass, @Nonnull final JsonParseNode itemNode) { - if (targetClass == Boolean.class) { - return (T) itemNode.getBooleanValue(); - } else if (targetClass == Short.class) { - return (T) itemNode.getShortValue(); - } else if (targetClass == Byte.class) { - return (T) itemNode.getByteValue(); - } else if (targetClass == BigDecimal.class) { - return (T) itemNode.getBigDecimalValue(); - } else if (targetClass == String.class) { - return (T) itemNode.getStringValue(); - } else if (targetClass == Integer.class) { - return (T) itemNode.getIntegerValue(); - } else if (targetClass == Float.class) { - return (T) itemNode.getFloatValue(); - } else if (targetClass == Double.class) { - return (T) itemNode.getDoubleValue(); - } else if (targetClass == Long.class) { - return (T) itemNode.getLongValue(); - } else if (targetClass == UUID.class) { - return (T) itemNode.getUUIDValue(); - } else if (targetClass == OffsetDateTime.class) { - return (T) itemNode.getOffsetDateTimeValue(); - } else if (targetClass == LocalDate.class) { - return (T) itemNode.getLocalDateValue(); - } else if (targetClass == LocalTime.class) { - return (T) itemNode.getLocalTimeValue(); - } else if (targetClass == PeriodAndDuration.class) { - return (T) itemNode.getPeriodAndDurationValue(); - } else { - throw new RuntimeException("unknown type to deserialize " + targetClass.getName()); - } + return gson.fromJson(itemNode.currentNode, targetClass); } private List iterateOnArray(JsonElement jsonElement, Function fn) { diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java index 4c0642871..0d61354ce 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java @@ -13,8 +13,21 @@ /** Creates new Json parse nodes from the payload. */ public class JsonParseNodeFactory implements ParseNodeFactory { + private final Gson gson; + /** Creates a new factory */ - public JsonParseNodeFactory() {} + public JsonParseNodeFactory() { + this(DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new factory + * @param gson the {@link Gson} instance to use for parsing value types. + */ + public JsonParseNodeFactory(@Nonnull Gson gson) { + Objects.requireNonNull(gson, "parameter gson cannot be null"); + this.gson = gson; + } /** {@inheritDoc} */ @Nonnull public String getValidContentType() { @@ -23,8 +36,6 @@ public JsonParseNodeFactory() {} private static final String validContentType = "application/json"; - private Gson gson = DefaultGsonBuilder.getDefaultInstance(); - /** * @return the {@link Gson} instance to use for parsing value types. */ @@ -32,14 +43,6 @@ public JsonParseNodeFactory() {} return gson; } - /** - * Specify a custom {@link Gson} instance for parsing value types. - * @param gson the {@link Gson} instance to use. - */ - public void setGson(@Nonnull Gson gson) { - this.gson = gson; - } - /** {@inheritDoc} */ @Override @Nonnull public ParseNode getParseNode( diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java index 0443d697c..af00b9d1e 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java @@ -38,12 +38,9 @@ public class JsonSerializationWriter implements SerializationWriter { /** * Creates a new instance of a json serialization writer - * @deprecated use {@link #JsonSerializationWriter(Gson)} instead. */ - @Deprecated public JsonSerializationWriter() { - this.writer = new JsonWriter(new OutputStreamWriter(this.stream, StandardCharsets.UTF_8)); - this.gson = DefaultGsonBuilder.getDefaultInstance(); + this(DefaultGsonBuilder.getDefaultInstance()); } /** diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java index a0fe43bc8..b1be011bd 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java @@ -8,8 +8,21 @@ /** Creates new Json serialization writers. */ public class JsonSerializationWriterFactory implements SerializationWriterFactory { + private final Gson gson; + /** Creates a new factory */ - public JsonSerializationWriterFactory() {} + public JsonSerializationWriterFactory() { + this(DefaultGsonBuilder.getDefaultInstance()); + } + + /** + * Creates a new factory + * @param gson the {@link Gson} instance to use for writing value types. + */ + public JsonSerializationWriterFactory(@Nonnull Gson gson) { + Objects.requireNonNull(gson, "gson contentType cannot be null"); + this.gson = gson; + } /** {@inheritDoc} */ @Nonnull public String getValidContentType() { @@ -18,8 +31,6 @@ public JsonSerializationWriterFactory() {} private static final String validContentType = "application/json"; - private Gson gson = DefaultGsonBuilder.getDefaultInstance(); - /** * @return the {@link Gson} instance to use for writing value types. */ @@ -27,14 +38,6 @@ public JsonSerializationWriterFactory() {} return gson; } - /** - * Specify a custom {@link Gson} instance for writing value types. - * @param gson the {@link Gson} instance to use. - */ - public void setGson(@Nonnull Gson gson) { - this.gson = gson; - } - /** {@inheritDoc} */ @Override @Nonnull public SerializationWriter getSerializationWriter(@Nonnull final String contentType) { From 89f34d6df32b1d0cd44131a72c2561170bf6d534 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Mon, 17 Nov 2025 22:05:13 +0100 Subject: [PATCH 03/11] Make getPrimitiveValue protected to allow sublcasses to change it's behavior --- .../java/com/microsoft/kiota/serialization/JsonParseNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index d375fb1b2..c3ff443f7 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -128,7 +128,7 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) return gson.fromJson(currentNode, PeriodAndDuration.class); } - @Nullable private T getPrimitiveValue( + @Nullable protected T getPrimitiveValue( @Nonnull final Class targetClass, @Nonnull final JsonParseNode itemNode) { return gson.fromJson(itemNode.currentNode, targetClass); } From 5c1ab086a39e6d8035d70cf43a5d1c3734f122af Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Mon, 17 Nov 2025 22:27:55 +0100 Subject: [PATCH 04/11] Removed checks for json primitive types before call to Gson --- .../java/com/microsoft/kiota/serialization/JsonParseNode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index c3ff443f7..7374aa32a 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -73,11 +73,11 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) } @Nullable public String getStringValue() { - return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, String.class) : null; + return gson.fromJson(currentNode, String.class); } @Nullable public Boolean getBooleanValue() { - return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Boolean.class) : null; + return gson.fromJson(currentNode, Boolean.class); } @Nullable public Byte getByteValue() { From 2ef01a122a63ae5d708c6586d30ecfe6c2cf4cb1 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Tue, 18 Nov 2025 21:46:00 +0100 Subject: [PATCH 05/11] Delegate writing to gson as well, add tests for writing and parsing all types and null --- .../kiota/serialization/JsonParseNode.java | 8 +-- .../serialization/JsonParseNodeFactory.java | 7 -- .../JsonSerializationWriter.java | 18 ++--- .../JsonSerializationWriterFactory.java | 7 -- .../JsonSerializationWriterTests.java | 62 ++++++++++++++++ .../kiota/serialization/TestParsable.java | 71 +++++++++++++++++++ .../mocks/IntersectionTypeMock.java | 21 +++--- .../serialization/mocks/UnionTypeMock.java | 19 +++-- 8 files changed, 170 insertions(+), 43 deletions(-) create mode 100644 components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index 7374aa32a..6e3169713 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -28,8 +28,8 @@ /** ParseNode implementation for JSON */ public class JsonParseNode implements ParseNode { - protected final JsonElement currentNode; - protected final Gson gson; + private final JsonElement currentNode; + private final Gson gson; /** * Creates a new instance of the JsonParseNode class. @@ -54,7 +54,7 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) * @param node the node to wrap. * @return the newly created {@link JsonParseNode}. */ - @Nonnull protected JsonParseNode createNewNode(@Nonnull JsonElement node) { + @Nonnull private JsonParseNode createNewNode(@Nonnull JsonElement node) { return new JsonParseNode(node, gson); } @@ -128,7 +128,7 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) return gson.fromJson(currentNode, PeriodAndDuration.class); } - @Nullable protected T getPrimitiveValue( + @Nullable private T getPrimitiveValue( @Nonnull final Class targetClass, @Nonnull final JsonParseNode itemNode) { return gson.fromJson(itemNode.currentNode, targetClass); } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java index 0d61354ce..65a8d4876 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNodeFactory.java @@ -36,13 +36,6 @@ public JsonParseNodeFactory(@Nonnull Gson gson) { private static final String validContentType = "application/json"; - /** - * @return the {@link Gson} instance to use for parsing value types. - */ - @Nonnull public Gson getGson() { - return gson; - } - /** {@inheritDoc} */ @Override @Nonnull public ParseNode getParseNode( diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java index af00b9d1e..0ef9ae474 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java @@ -58,7 +58,7 @@ public void writeStringValue(@Nullable final String key, @Nullable final String if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(String.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -70,7 +70,7 @@ public void writeBooleanValue(@Nullable final String key, @Nullable final Boolea if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Boolean.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -82,7 +82,7 @@ public void writeShortValue(@Nullable final String key, @Nullable final Short va if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Short.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -94,7 +94,7 @@ public void writeByteValue(@Nullable final String key, @Nullable final Byte valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Byte.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -106,7 +106,7 @@ public void writeBigDecimalValue(@Nullable final String key, @Nullable final Big if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(BigDecimal.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -118,7 +118,7 @@ public void writeIntegerValue(@Nullable final String key, @Nullable final Intege if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Integer.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -130,7 +130,7 @@ public void writeFloatValue(@Nullable final String key, @Nullable final Float va if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Float.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -142,7 +142,7 @@ public void writeDoubleValue(@Nullable final String key, @Nullable final Double if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Double.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } @@ -154,7 +154,7 @@ public void writeLongValue(@Nullable final String key, @Nullable final Long valu if (key != null && !key.isEmpty()) { writer.name(key); } - writer.value(value); + gson.getAdapter(Long.class).write(writer, value); } catch (IOException ex) { throw new RuntimeException("could not serialize value", ex); } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java index b1be011bd..f6420a1c0 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriterFactory.java @@ -31,13 +31,6 @@ public JsonSerializationWriterFactory(@Nonnull Gson gson) { private static final String validContentType = "application/json"; - /** - * @return the {@link Gson} instance to use for writing value types. - */ - @Nonnull public Gson getGson() { - return gson; - } - /** {@inheritDoc} */ @Override @Nonnull public SerializationWriter getSerializationWriter(@Nonnull final String contentType) { diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java index 5a3834771..047e389db 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java @@ -1,8 +1,10 @@ package com.microsoft.kiota.serialization; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import com.microsoft.kiota.Compatibility; +import com.microsoft.kiota.PeriodAndDuration; import com.microsoft.kiota.serialization.mocks.MyEnum; import com.microsoft.kiota.serialization.mocks.TestEntity; import com.microsoft.kiota.serialization.mocks.UntypedTestEntity; @@ -10,11 +12,17 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.math.BigDecimal; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.Period; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.UUID; class JsonSerializationWriterTests { @@ -225,4 +233,58 @@ void writesSampleObjectValueWithUntypedProperties() throws IOException { assertEquals(expectedString, serializedJsonString); } } + + @Test + void parseWrittenValues() throws IOException { + writeAndParse( + ParseNode::getStringValue, SerializationWriter::writeStringValue, "just a string"); + writeAndParse(ParseNode::getBooleanValue, SerializationWriter::writeBooleanValue, true); + writeAndParse(ParseNode::getByteValue, SerializationWriter::writeByteValue, (byte) 3); + writeAndParse(ParseNode::getShortValue, SerializationWriter::writeShortValue, (short) 42); + writeAndParse( + ParseNode::getBigDecimalValue, + SerializationWriter::writeBigDecimalValue, + new BigDecimal(123456789L)); + writeAndParse(ParseNode::getIntegerValue, SerializationWriter::writeIntegerValue, 54321); + writeAndParse( + ParseNode::getFloatValue, SerializationWriter::writeFloatValue, (float) 67.89); + writeAndParse(ParseNode::getDoubleValue, SerializationWriter::writeDoubleValue, 3245.12356); + writeAndParse(ParseNode::getLongValue, SerializationWriter::writeLongValue, -543219876L); + writeAndParse( + ParseNode::getUUIDValue, SerializationWriter::writeUUIDValue, UUID.randomUUID()); + writeAndParse( + ParseNode::getOffsetDateTimeValue, + SerializationWriter::writeOffsetDateTimeValue, + OffsetDateTime.now()); + writeAndParse( + ParseNode::getLocalDateValue, + SerializationWriter::writeLocalDateValue, + LocalDate.now()); + writeAndParse( + ParseNode::getLocalTimeValue, + SerializationWriter::writeLocalTimeValue, + LocalTime.now()); + writeAndParse( + ParseNode::getPeriodAndDurationValue, + SerializationWriter::writePeriodAndDurationValue, + PeriodAndDuration.of(Period.ofYears(3), Duration.ofHours(6))); + } + + private void writeAndParse( + TestParsable.ParseMethod parseMethod, + TestParsable.WriteMethod writeMethod, + T value) + throws IOException { + var testParsable = new TestParsable<>(parseMethod, writeMethod, value); + var writer = new JsonSerializationWriter(DefaultGsonBuilder.getDefaultInstance()); + writer.writeObjectValue(null, testParsable); + + var parseNodeFactory = new JsonParseNodeFactory(DefaultGsonBuilder.getDefaultInstance()); + var parseNode = + parseNodeFactory.getParseNode("application/json", writer.getSerializedContent()); + var result = parseNode.getObjectValue(TestParsable.factory(parseMethod, writeMethod)); + assertEquals(value, result.getRealValue()); + assertNull(result.getNullValue()); + writer.close(); + } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java new file mode 100644 index 000000000..c0dd35fb9 --- /dev/null +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/TestParsable.java @@ -0,0 +1,71 @@ +package com.microsoft.kiota.serialization; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public class TestParsable implements Parsable { + @FunctionalInterface + public interface WriteMethod { + public void write(SerializationWriter writer, String key, V value); + } + + @FunctionalInterface + public interface ParseMethod { + public V parse(ParseNode parseNode); + } + + private T realValue; + + private T nullValue; + + private final ParseMethod parseMethod; + + private final WriteMethod writeMethod; + + public TestParsable(ParseMethod parseMethod, WriteMethod writeMethod) { + this(parseMethod, writeMethod, null); + } + + public TestParsable(ParseMethod parseMethod, WriteMethod writeMethod, T value) { + this.parseMethod = parseMethod; + this.writeMethod = writeMethod; + this.realValue = value; + } + + @Override + public Map> getFieldDeserializers() { + final HashMap> deserializerMap = + new HashMap>(2); + deserializerMap.put( + "realValue", + (n) -> { + this.realValue = parseMethod.parse(n); + }); + deserializerMap.put( + "nullValue", + (n) -> { + this.nullValue = parseMethod.parse(n); + }); + return deserializerMap; + } + + @Override + public void serialize(SerializationWriter writer) { + writeMethod.write(writer, "realValue", realValue); + writeMethod.write(writer, "nullValue", nullValue); + } + + public T getRealValue() { + return realValue; + } + + public T getNullValue() { + return nullValue; + } + + public static ParsableFactory> factory( + ParseMethod parseMethod, WriteMethod writeMethod) { + return (n) -> new TestParsable(parseMethod, writeMethod); + } +} diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java index c0b370419..2f05f9869 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization.mocks; +import com.google.gson.JsonSyntaxException; import com.microsoft.kiota.serialization.ComposedTypeWrapper; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.ParseNode; @@ -21,16 +22,18 @@ public class IntersectionTypeMock implements Parsable, ComposedTypeWrapper { @jakarta.annotation.Nonnull final ParseNode parseNode) { Objects.requireNonNull(parseNode); final var result = new IntersectionTypeMock(); - if (parseNode.getStringValue() != null) { + try { result.setStringValue(parseNode.getStringValue()); - } else if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) - != null) { - result.setComposedType3( - parseNode.getCollectionOfObjectValues( - TestEntity::createFromDiscriminatorValue)); - } else { - result.setComposedType1(new TestEntity()); - result.setComposedType2(new SecondTestEntity()); + } catch (JsonSyntaxException e) { + if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) + != null) { + result.setComposedType3( + parseNode.getCollectionOfObjectValues( + TestEntity::createFromDiscriminatorValue)); + } else { + result.setComposedType1(new TestEntity()); + result.setComposedType2(new SecondTestEntity()); + } } return result; } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java index d2e6e18b0..2d5d56382 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization.mocks; +import com.google.gson.JsonSyntaxException; import com.microsoft.kiota.serialization.ComposedTypeWrapper; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.ParseNode; @@ -28,13 +29,17 @@ public class UnionTypeMock implements Parsable, ComposedTypeWrapper { } else if ("#microsoft.graph.secondTestEntity".equalsIgnoreCase(mappingValue)) { result.setComposedType2(new SecondTestEntity()); } - } else if (parseNode.getStringValue() != null) { - result.setStringValue(parseNode.getStringValue()); - } else if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) - != null) { - result.setComposedType3( - parseNode.getCollectionOfObjectValues( - TestEntity::createFromDiscriminatorValue)); + } else { + try { + result.setStringValue(parseNode.getStringValue()); + } catch (JsonSyntaxException e) { + if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) + != null) { + result.setComposedType3( + parseNode.getCollectionOfObjectValues( + TestEntity::createFromDiscriminatorValue)); + } + } } return result; } From 16964b22b0ba0f099dba34b7d2ddbfc90fbdbfba Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Tue, 18 Nov 2025 22:27:42 +0100 Subject: [PATCH 06/11] Delegate byte[] to gson as well for parsing and writing --- .../serialization/DefaultGsonBuilder.java | 30 ++++++++++++++++++- .../kiota/serialization/JsonParseNode.java | 7 +---- .../JsonSerializationWriter.java | 11 +++++-- .../JsonSerializationWriterTests.java | 11 ++++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java index cd70927aa..9c54647f7 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -15,6 +15,7 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeParseException; +import java.util.Base64; public class DefaultGsonBuilder { @@ -116,6 +117,32 @@ public void write(JsonWriter out, PeriodAndDuration value) throws IOException { } }; + private static final TypeAdapter BYTE_ARRAY = + new TypeAdapter<>() { + @Override + public byte[] read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + try { + if (stringValue.isEmpty()) { + return null; + } + return Base64.getDecoder().decode(stringValue); + } catch (IllegalArgumentException ex) { + throw new JsonSyntaxException( + "Failed parsing '" + + stringValue + + "' as byte[]; at path " + + in.getPreviousPath(), + ex); + } + } + + @Override + public void write(JsonWriter out, byte[] value) throws IOException { + out.value(Base64.getEncoder().encodeToString(value)); + } + }; + private static final Gson defaultInstance = getDefaultBuilder().create(); public static Gson getDefaultInstance() { @@ -127,6 +154,7 @@ public static GsonBuilder getDefaultBuilder() { .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME.nullSafe()) .registerTypeAdapter(LocalDate.class, LOCAL_DATE.nullSafe()) .registerTypeAdapter(LocalTime.class, LOCAL_TIME.nullSafe()) - .registerTypeAdapter(PeriodAndDuration.class, PERIOD_AND_DURATION.nullSafe()); + .registerTypeAdapter(PeriodAndDuration.class, PERIOD_AND_DURATION.nullSafe()) + .registerTypeAdapter(byte[].class, BYTE_ARRAY.nullSafe()); } } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index 6e3169713..cff0b8504 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -15,7 +15,6 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.Base64; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; @@ -312,10 +311,6 @@ public void setOnAfterAssignFieldValues(@Nullable final Consumer value } @Nullable public byte[] getByteArrayValue() { - final String base64 = this.getStringValue(); - if (base64 == null || base64.isEmpty()) { - return null; - } - return Base64.getDecoder().decode(base64); + return gson.fromJson(currentNode, byte[].class); } } diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java index 0ef9ae474..11ca51d79 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonSerializationWriter.java @@ -18,7 +18,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.OffsetDateTime; -import java.util.Base64; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -540,6 +539,14 @@ public void setOnStartObjectSerialization( } public void writeByteArrayValue(@Nullable final String key, @Nullable final byte[] value) { - if (value != null) this.writeStringValue(key, Base64.getEncoder().encodeToString(value)); + if (value != null) + try { + if (key != null && !key.isEmpty()) { + writer.name(key); + } + gson.getAdapter(byte[].class).write(writer, value); + } catch (IOException ex) { + throw new RuntimeException("could not serialize value", ex); + } } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java index 047e389db..9bdac251f 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/JsonSerializationWriterTests.java @@ -1,5 +1,6 @@ package com.microsoft.kiota.serialization; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -268,6 +269,10 @@ void parseWrittenValues() throws IOException { ParseNode::getPeriodAndDurationValue, SerializationWriter::writePeriodAndDurationValue, PeriodAndDuration.of(Period.ofYears(3), Duration.ofHours(6))); + writeAndParse( + ParseNode::getByteArrayValue, + SerializationWriter::writeByteArrayValue, + new byte[] {8, 10, 127, -50, 0}); } private void writeAndParse( @@ -283,7 +288,11 @@ private void writeAndParse( var parseNode = parseNodeFactory.getParseNode("application/json", writer.getSerializedContent()); var result = parseNode.getObjectValue(TestParsable.factory(parseMethod, writeMethod)); - assertEquals(value, result.getRealValue()); + if (value instanceof byte[] bytea) { + assertArrayEquals(bytea, (byte[]) result.getRealValue()); + } else { + assertEquals(value, result.getRealValue()); + } assertNull(result.getNullValue()); writer.close(); } From 17dcc4840b23cdcbe2c959ed83185d228dbaa116 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Wed, 19 Nov 2025 18:57:04 +0100 Subject: [PATCH 07/11] Revert changes to mock, reintroduce checks for primitive values --- .../kiota/serialization/JsonParseNode.java | 20 +++++++++--------- .../mocks/IntersectionTypeMock.java | 21 ++++++++----------- .../serialization/mocks/UnionTypeMock.java | 19 +++++++---------- 3 files changed, 26 insertions(+), 34 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index cff0b8504..debf21392 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -72,39 +72,39 @@ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) } @Nullable public String getStringValue() { - return gson.fromJson(currentNode, String.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, String.class) : null; } @Nullable public Boolean getBooleanValue() { - return gson.fromJson(currentNode, Boolean.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Boolean.class) : null; } @Nullable public Byte getByteValue() { - return gson.fromJson(currentNode, Byte.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Byte.class) : null; } @Nullable public Short getShortValue() { - return gson.fromJson(currentNode, Short.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Short.class) : null; } @Nullable public BigDecimal getBigDecimalValue() { - return gson.fromJson(currentNode, BigDecimal.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, BigDecimal.class) : null; } @Nullable public Integer getIntegerValue() { - return gson.fromJson(currentNode, Integer.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Integer.class) : null; } @Nullable public Float getFloatValue() { - return gson.fromJson(currentNode, Float.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Float.class) : null; } @Nullable public Double getDoubleValue() { - return gson.fromJson(currentNode, Double.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Double.class) : null; } @Nullable public Long getLongValue() { - return gson.fromJson(currentNode, Long.class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, Long.class) : null; } @Nullable public UUID getUUIDValue() { @@ -311,6 +311,6 @@ public void setOnAfterAssignFieldValues(@Nullable final Consumer value } @Nullable public byte[] getByteArrayValue() { - return gson.fromJson(currentNode, byte[].class); + return currentNode.isJsonPrimitive() ? gson.fromJson(currentNode, byte[].class) : null; } } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java index 2f05f9869..c0b370419 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/IntersectionTypeMock.java @@ -1,6 +1,5 @@ package com.microsoft.kiota.serialization.mocks; -import com.google.gson.JsonSyntaxException; import com.microsoft.kiota.serialization.ComposedTypeWrapper; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.ParseNode; @@ -22,18 +21,16 @@ public class IntersectionTypeMock implements Parsable, ComposedTypeWrapper { @jakarta.annotation.Nonnull final ParseNode parseNode) { Objects.requireNonNull(parseNode); final var result = new IntersectionTypeMock(); - try { + if (parseNode.getStringValue() != null) { result.setStringValue(parseNode.getStringValue()); - } catch (JsonSyntaxException e) { - if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) - != null) { - result.setComposedType3( - parseNode.getCollectionOfObjectValues( - TestEntity::createFromDiscriminatorValue)); - } else { - result.setComposedType1(new TestEntity()); - result.setComposedType2(new SecondTestEntity()); - } + } else if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) + != null) { + result.setComposedType3( + parseNode.getCollectionOfObjectValues( + TestEntity::createFromDiscriminatorValue)); + } else { + result.setComposedType1(new TestEntity()); + result.setComposedType2(new SecondTestEntity()); } return result; } diff --git a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java index 2d5d56382..d2e6e18b0 100644 --- a/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java +++ b/components/serialization/json/src/test/java/com/microsoft/kiota/serialization/mocks/UnionTypeMock.java @@ -1,6 +1,5 @@ package com.microsoft.kiota.serialization.mocks; -import com.google.gson.JsonSyntaxException; import com.microsoft.kiota.serialization.ComposedTypeWrapper; import com.microsoft.kiota.serialization.Parsable; import com.microsoft.kiota.serialization.ParseNode; @@ -29,17 +28,13 @@ public class UnionTypeMock implements Parsable, ComposedTypeWrapper { } else if ("#microsoft.graph.secondTestEntity".equalsIgnoreCase(mappingValue)) { result.setComposedType2(new SecondTestEntity()); } - } else { - try { - result.setStringValue(parseNode.getStringValue()); - } catch (JsonSyntaxException e) { - if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) - != null) { - result.setComposedType3( - parseNode.getCollectionOfObjectValues( - TestEntity::createFromDiscriminatorValue)); - } - } + } else if (parseNode.getStringValue() != null) { + result.setStringValue(parseNode.getStringValue()); + } else if (parseNode.getCollectionOfObjectValues(TestEntity::createFromDiscriminatorValue) + != null) { + result.setComposedType3( + parseNode.getCollectionOfObjectValues( + TestEntity::createFromDiscriminatorValue)); } return result; } From 05271915f488dd331a862ce310a8ef254dd13a0b Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Thu, 20 Nov 2025 15:17:55 +0100 Subject: [PATCH 08/11] Make code compile with java8 profile --- .../serialization/json/spotBugsExcludeFilter.xml | 3 ++- .../kiota/serialization/DefaultGsonBuilder.java | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/components/serialization/json/spotBugsExcludeFilter.xml b/components/serialization/json/spotBugsExcludeFilter.xml index d0f06c8a8..4a40a57fb 100644 --- a/components/serialization/json/spotBugsExcludeFilter.xml +++ b/components/serialization/json/spotBugsExcludeFilter.xml @@ -18,6 +18,7 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu + @@ -31,4 +32,4 @@ xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubu - \ No newline at end of file + diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java index 9c54647f7..19a695616 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -20,7 +20,7 @@ public class DefaultGsonBuilder { private static final TypeAdapter OFFSET_DATE_TIME = - new TypeAdapter<>() { + new TypeAdapter() { @Override public OffsetDateTime read(JsonReader in) throws IOException { String stringValue = in.nextString(); @@ -49,7 +49,7 @@ public void write(JsonWriter out, OffsetDateTime value) throws IOException { }; private static final TypeAdapter LOCAL_DATE = - new TypeAdapter<>() { + new TypeAdapter() { @Override public LocalDate read(JsonReader in) throws IOException { String stringValue = in.nextString(); @@ -72,7 +72,7 @@ public void write(JsonWriter out, LocalDate value) throws IOException { }; private static final TypeAdapter LOCAL_TIME = - new TypeAdapter<>() { + new TypeAdapter() { @Override public LocalTime read(JsonReader in) throws IOException { String stringValue = in.nextString(); @@ -95,7 +95,7 @@ public void write(JsonWriter out, LocalTime value) throws IOException { }; private static final TypeAdapter PERIOD_AND_DURATION = - new TypeAdapter<>() { + new TypeAdapter() { @Override public PeriodAndDuration read(JsonReader in) throws IOException { String stringValue = in.nextString(); @@ -118,7 +118,7 @@ public void write(JsonWriter out, PeriodAndDuration value) throws IOException { }; private static final TypeAdapter BYTE_ARRAY = - new TypeAdapter<>() { + new TypeAdapter() { @Override public byte[] read(JsonReader in) throws IOException { String stringValue = in.nextString(); From e444fdacb6c322e60a37ef75154d5ea4bd3b0fdd Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 20 Nov 2025 09:28:20 -0500 Subject: [PATCH 09/11] docs: duplicated doc comment for the gson parameter --- .../java/com/microsoft/kiota/serialization/JsonParseNode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java index debf21392..f7b006596 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/JsonParseNode.java @@ -41,7 +41,7 @@ public JsonParseNode(@Nonnull final JsonElement node) { /** * Creates a new instance of the JsonParseNode class. * @param node the node to wrap. - * @param node the node to wrap. + * @param gson the Gson instance to use and deserialize the node with. */ public JsonParseNode(@Nonnull final JsonElement node, @Nonnull final Gson gson) { currentNode = Objects.requireNonNull(node, "parameter node cannot be null"); From c2ac9d6fff0e850c785db136e7482be59fa3fffa Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Thu, 20 Nov 2025 15:44:20 +0100 Subject: [PATCH 10/11] Fix serialization of date/time types using ISO formats --- .../microsoft/kiota/serialization/DefaultGsonBuilder.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java index 19a695616..37e0eed86 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -14,6 +14,7 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Base64; @@ -44,7 +45,7 @@ public OffsetDateTime read(JsonReader in) throws IOException { @Override public void write(JsonWriter out, OffsetDateTime value) throws IOException { - out.value(value.toString()); + out.value(value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); } }; @@ -67,7 +68,7 @@ public LocalDate read(JsonReader in) throws IOException { @Override public void write(JsonWriter out, LocalDate value) throws IOException { - out.value(value.toString()); + out.value(value.format(DateTimeFormatter.ISO_LOCAL_DATE)); } }; @@ -90,7 +91,7 @@ public LocalTime read(JsonReader in) throws IOException { @Override public void write(JsonWriter out, LocalTime value) throws IOException { - out.value(value.toString()); + out.value(value.format(DateTimeFormatter.ISO_LOCAL_TIME)); } }; From 593bbf797c1f7a6c0d277c4ba4d749dd40d1f6b5 Mon Sep 17 00:00:00 2001 From: Emond Papegaaij Date: Thu, 20 Nov 2025 15:54:29 +0100 Subject: [PATCH 11/11] Add Nonnull to default gson instances --- .../microsoft/kiota/serialization/DefaultGsonBuilder.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java index 37e0eed86..f19a651fb 100644 --- a/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java +++ b/components/serialization/json/src/main/java/com/microsoft/kiota/serialization/DefaultGsonBuilder.java @@ -8,6 +8,8 @@ import com.google.gson.stream.JsonWriter; import com.microsoft.kiota.PeriodAndDuration; +import jakarta.annotation.Nonnull; + import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; @@ -146,11 +148,11 @@ public void write(JsonWriter out, byte[] value) throws IOException { private static final Gson defaultInstance = getDefaultBuilder().create(); - public static Gson getDefaultInstance() { + public static @Nonnull Gson getDefaultInstance() { return defaultInstance; } - public static GsonBuilder getDefaultBuilder() { + public static @Nonnull GsonBuilder getDefaultBuilder() { return new GsonBuilder() .registerTypeAdapter(OffsetDateTime.class, OFFSET_DATE_TIME.nullSafe()) .registerTypeAdapter(LocalDate.class, LOCAL_DATE.nullSafe())