From 4b893f8e22e23e593e24a3033225b82eac7ec059 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 18 Aug 2026 10:21:40 +0200 Subject: [PATCH 1/4] feat: support composed types in write_object_value Update write_object_value to accept variadic additional_values_to_merge for intersection type serialization and handle ComposedTypeWrapper by serializing directly into the current writer instead of a temp. Refactor write_collection_of_object_values to create its own temp writers instead of delegating to write_object_value, since the nil-key path now serializes into self. --- .../json_serialization_writer.rb | 28 +- ...serialization_writer_composed_type_spec.rb | 276 ++++++++++++++++++ 2 files changed, 289 insertions(+), 15 deletions(-) create mode 100644 spec/json_serialization_writer_composed_type_spec.rb diff --git a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb index c4aeacb..b64acee 100644 --- a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb +++ b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb @@ -146,30 +146,28 @@ def write_collection_of_primitive_values(key, values) def write_collection_of_object_values(key, values) if values - if !key - return values.map do |v| - self.write_object_value(nil, v) - end + serialized = values.map do |v| + temp = JsonSerializationWriter.new() + v.serialize(temp) + temp end - @writer[key] = values.map do |v| - self.write_object_value(nil, v).writer + if !key + return serialized end + @writer[key] = serialized.map(&:writer) end end - def write_object_value(key, value) + def write_object_value(key, value, *additional_values_to_merge) if value - if !key - temp = JsonSerializationWriter.new() - value.serialize(temp) - return temp - end - begin + if key temp = JsonSerializationWriter.new() value.serialize(temp) + additional_values_to_merge.each { |v| v.serialize(temp) unless v.nil? } @writer[key] = temp.writer - rescue StandardError => e - raise e.class, "no key or value included in write_boolean_value(key, value)" + else + value.serialize(self) + additional_values_to_merge.each { |v| v.serialize(self) unless v.nil? } end end end diff --git a/spec/json_serialization_writer_composed_type_spec.rb b/spec/json_serialization_writer_composed_type_spec.rb new file mode 100644 index 0000000..4374bca --- /dev/null +++ b/spec/json_serialization_writer_composed_type_spec.rb @@ -0,0 +1,276 @@ +# frozen_string_literal: true + +require_relative 'spec_helper' +require 'microsoft_kiota_abstractions' +require 'microsoft_kiota_serialization_json' + +module TestModels + class SimpleParsable + include MicrosoftKiotaAbstractions::Parsable + + attr_accessor :name, :age + + def get_field_deserializers + { + 'name' => lambda { |n| @name = n.get_string_value }, + 'age' => lambda { |n| @age = n.get_number_value } + } + end + + def serialize(writer) + writer.write_string_value('name', @name) + writer.write_number_value('age', @age) + end + + def self.create_from_discriminator_value(_parse_node) + SimpleParsable.new + end + end + + class AnotherParsable + include MicrosoftKiotaAbstractions::Parsable + + attr_accessor :email + + def get_field_deserializers + { 'email' => lambda { |n| @email = n.get_string_value } } + end + + def serialize(writer) + writer.write_string_value('email', @email) + end + + def self.create_from_discriminator_value(_parse_node) + AnotherParsable.new + end + end + + class UnionTypeWrapper + include MicrosoftKiotaAbstractions::Parsable + include MicrosoftKiotaAbstractions::ComposedTypeWrapper + + attr_accessor :simple, :another + + def get_field_deserializers + return @simple.get_field_deserializers if @simple + return @another.get_field_deserializers if @another + {} + end + + def serialize(writer) + if @simple + writer.write_object_value(nil, @simple) + elsif @another + writer.write_object_value(nil, @another) + end + end + + def self.create_from_discriminator_value(_parse_node) + UnionTypeWrapper.new + end + end + + class IntersectionTypeWrapper + include MicrosoftKiotaAbstractions::Parsable + include MicrosoftKiotaAbstractions::ComposedTypeWrapper + + attr_accessor :simple, :another + + def get_field_deserializers + MicrosoftKiotaAbstractions::ParseNodeHelper + .merge_deserializers_for_intersection_wrapper(@simple, @another) + end + + def serialize(writer) + writer.write_object_value(nil, @simple, @another) + end + + def self.create_from_discriminator_value(_parse_node) + IntersectionTypeWrapper.new + end + end +end + +RSpec.describe MicrosoftKiotaSerializationJson::JsonSerializationWriter do + describe '#write_object_value' do + it 'serializes a regular object with a key' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj = TestModels::SimpleParsable.new + obj.name = 'Alice' + obj.age = 30 + + writer.write_object_value('person', obj) + expect(writer.writer['person']).to eq({ 'name' => 'Alice', 'age' => 30 }) + end + + it 'serializes a regular object with additional values merged under a key' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Alice' + obj1.age = 30 + obj2 = TestModels::AnotherParsable.new + obj2.email = 'alice@example.com' + + writer.write_object_value('person', obj1, obj2) + expect(writer.writer['person']).to eq({ 'name' => 'Alice', 'age' => 30, 'email' => 'alice@example.com' }) + end + + it 'skips nil additional values' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj = TestModels::SimpleParsable.new + obj.name = 'Bob' + obj.age = 22 + + writer.write_object_value('person', obj, nil, nil) + expect(writer.writer['person']).to eq({ 'name' => 'Bob', 'age' => 22 }) + end + + it 'returns nil when value is nil' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + result = writer.write_object_value('key', nil) + expect(result).to be_nil + expect(writer.writer).to eq({}) + end + + it 'serializes into self when key is nil for a non-composed type' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj = TestModels::SimpleParsable.new + obj.name = 'Eve' + obj.age = 40 + + writer.write_object_value(nil, obj) + expect(writer.writer).to eq({ 'name' => 'Eve', 'age' => 40 }) + end + end + + describe '#write_object_value with union types' do + it 'serializes the first branch of a union into the current writer' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + union = TestModels::UnionTypeWrapper.new + inner = TestModels::SimpleParsable.new + inner.name = 'Charlie' + inner.age = 25 + union.simple = inner + + writer.write_object_value(nil, union) + expect(writer.writer).to eq({ 'name' => 'Charlie', 'age' => 25 }) + end + + it 'serializes the second branch of a union into the current writer' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + union = TestModels::UnionTypeWrapper.new + inner = TestModels::AnotherParsable.new + inner.email = 'test@example.com' + union.another = inner + + writer.write_object_value(nil, union) + expect(writer.writer).to eq({ 'email' => 'test@example.com' }) + end + + it 'produces valid JSON for a union type' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + union = TestModels::UnionTypeWrapper.new + inner = TestModels::SimpleParsable.new + inner.name = 'Charlie' + inner.age = 25 + union.simple = inner + + writer.write_object_value(nil, union) + json = writer.get_serialized_content + parsed = JSON.parse(json) + expect(parsed).to eq({ 'name' => 'Charlie', 'age' => 25 }) + end + end + + describe '#write_object_value with intersection types' do + it 'merges all intersection members into the current writer' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + intersection = TestModels::IntersectionTypeWrapper.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Dana' + obj1.age = 28 + obj2 = TestModels::AnotherParsable.new + obj2.email = 'dana@example.com' + intersection.simple = obj1 + intersection.another = obj2 + + writer.write_object_value(nil, intersection) + expect(writer.writer).to eq({ 'name' => 'Dana', 'age' => 28, 'email' => 'dana@example.com' }) + end + + it 'produces valid JSON for an intersection type' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + intersection = TestModels::IntersectionTypeWrapper.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Dana' + obj1.age = 28 + obj2 = TestModels::AnotherParsable.new + obj2.email = 'dana@example.com' + intersection.simple = obj1 + intersection.another = obj2 + + writer.write_object_value(nil, intersection) + json = writer.get_serialized_content + parsed = JSON.parse(json) + expect(parsed).to eq({ 'name' => 'Dana', 'age' => 28, 'email' => 'dana@example.com' }) + end + end + + describe '#write_collection_of_object_values' do + it 'serializes a collection of objects under a key' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Alice' + obj1.age = 30 + obj2 = TestModels::SimpleParsable.new + obj2.name = 'Bob' + obj2.age = 25 + + writer.write_collection_of_object_values('people', [obj1, obj2]) + expect(writer.writer['people']).to eq([ + { 'name' => 'Alice', 'age' => 30 }, + { 'name' => 'Bob', 'age' => 25 } + ]) + end + + it 'returns temp writers when key is nil' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Alice' + obj1.age = 30 + + result = writer.write_collection_of_object_values(nil, [obj1]) + expect(result).to be_an(Array) + expect(result.length).to eq(1) + expect(result[0]).to be_a(MicrosoftKiotaSerializationJson::JsonSerializationWriter) + expect(result[0].writer).to eq({ 'name' => 'Alice', 'age' => 30 }) + end + + it 'does not contaminate the parent writer' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + writer.write_string_value('top_level', 'value') + obj = TestModels::SimpleParsable.new + obj.name = 'Alice' + obj.age = 30 + + writer.write_collection_of_object_values('people', [obj]) + expect(writer.writer.keys).to contain_exactly('top_level', 'people') + end + + it 'produces valid JSON with a collection' do + writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new + obj1 = TestModels::SimpleParsable.new + obj1.name = 'Alice' + obj1.age = 30 + obj2 = TestModels::AnotherParsable.new + obj2.email = 'bob@example.com' + + writer.write_collection_of_object_values('items', [obj1]) + writer.write_object_value('contact', obj2) + json = writer.get_serialized_content + parsed = JSON.parse(json) + expect(parsed['items']).to eq([{ 'name' => 'Alice', 'age' => 30 }]) + expect(parsed['contact']).to eq({ 'email' => 'bob@example.com' }) + end + end +end From d7c1a0109b2cde55c0016e36575f6ebfd0500137 Mon Sep 17 00:00:00 2001 From: andreatp Date: Tue, 18 Aug 2026 13:53:56 +0200 Subject: [PATCH 2/4] fix: remove temp writer pattern from write_collection_of_object_values Delegate to write_object_value instead of creating temp JsonSerializationWriter instances directly, aligning with how other Kiota language implementations handle collection serialization. Also removes redundant require and adds ComposedTypeWrapper polyfill for the test suite. --- .../json_serialization_writer.rb | 12 +++++------ ...serialization_writer_composed_type_spec.rb | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb index b64acee..271fc11 100644 --- a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb +++ b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb @@ -146,15 +146,13 @@ def write_collection_of_primitive_values(key, values) def write_collection_of_object_values(key, values) if values - serialized = values.map do |v| - temp = JsonSerializationWriter.new() - v.serialize(temp) - temp - end if !key - return serialized + return values.map { |v| write_object_value(nil, v) } + end + @writer[key] = values.map do |v| + write_object_value(key, v) + @writer.delete(key) end - @writer[key] = serialized.map(&:writer) end end diff --git a/spec/json_serialization_writer_composed_type_spec.rb b/spec/json_serialization_writer_composed_type_spec.rb index 4374bca..5d3d742 100644 --- a/spec/json_serialization_writer_composed_type_spec.rb +++ b/spec/json_serialization_writer_composed_type_spec.rb @@ -2,7 +2,16 @@ require_relative 'spec_helper' require 'microsoft_kiota_abstractions' -require 'microsoft_kiota_serialization_json' + +module MicrosoftKiotaAbstractions + module ComposedTypeWrapper; end + + module ParseNodeHelper + def self.merge_deserializers_for_intersection_wrapper(*parsables) + parsables.compact.reduce({}) { |acc, p| acc.merge(p.get_field_deserializers) } + end + end +end unless defined?(MicrosoftKiotaAbstractions::ComposedTypeWrapper) module TestModels class SimpleParsable @@ -233,17 +242,14 @@ def self.create_from_discriminator_value(_parse_node) ]) end - it 'returns temp writers when key is nil' do + it 'serializes into self when key is nil' do writer = MicrosoftKiotaSerializationJson::JsonSerializationWriter.new obj1 = TestModels::SimpleParsable.new obj1.name = 'Alice' obj1.age = 30 - result = writer.write_collection_of_object_values(nil, [obj1]) - expect(result).to be_an(Array) - expect(result.length).to eq(1) - expect(result[0]).to be_a(MicrosoftKiotaSerializationJson::JsonSerializationWriter) - expect(result[0].writer).to eq({ 'name' => 'Alice', 'age' => 30 }) + writer.write_collection_of_object_values(nil, [obj1]) + expect(writer.writer).to eq({ 'name' => 'Alice', 'age' => 30 }) end it 'does not contaminate the parent writer' do From 0a555ba4abcc42f5eda9aeedec9126bd80d388c2 Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 20 Aug 2026 12:40:16 +0100 Subject: [PATCH 3/4] refactor: extract object_value_hash helper for object serialization Centralizes temp writer creation into a private helper shared by write_object_value and write_collection_of_object_values, eliminating the fragile write-then-delete pattern from the keyed collection path. --- .../json_serialization_writer.rb | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb index 271fc11..5856f8c 100644 --- a/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb +++ b/lib/microsoft_kiota_serialization_json/json_serialization_writer.rb @@ -149,20 +149,14 @@ def write_collection_of_object_values(key, values) if !key return values.map { |v| write_object_value(nil, v) } end - @writer[key] = values.map do |v| - write_object_value(key, v) - @writer.delete(key) - end + @writer[key] = values.map { |v| object_value_hash(v) } end end def write_object_value(key, value, *additional_values_to_merge) if value if key - temp = JsonSerializationWriter.new() - value.serialize(temp) - additional_values_to_merge.each { |v| v.serialize(temp) unless v.nil? } - @writer[key] = temp.writer + @writer[key] = object_value_hash(value, *additional_values_to_merge) else value.serialize(self) additional_values_to_merge.each { |v| v.serialize(self) unless v.nil? } @@ -187,6 +181,17 @@ def write_additional_data(value) end end + private + + def object_value_hash(value, *additional_values_to_merge) + temp = JsonSerializationWriter.new + value.serialize(temp) + additional_values_to_merge.each { |v| v.serialize(temp) unless v.nil? } + temp.writer + end + + public + def write_any_value(key, value) if value if !!value == value From c7776c5f167e0647e6c3c37ac5639b91b9612f51 Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 20 Aug 2026 16:33:49 +0100 Subject: [PATCH 4/4] chore: bump microsoft_kiota_abstractions to >= 0.16 Requires v0.16.0 which includes ComposedTypeWrapper, ParseNodeHelper, and the updated write_object_value signature needed for composed type support. Removes the test polyfill that was standing in for these. --- microsoft_kiota_serialization_json.gemspec | 2 +- spec/json_serialization_writer_composed_type_spec.rb | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/microsoft_kiota_serialization_json.gemspec b/microsoft_kiota_serialization_json.gemspec index 5202cbf..27363e0 100644 --- a/microsoft_kiota_serialization_json.gemspec +++ b/microsoft_kiota_serialization_json.gemspec @@ -30,7 +30,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_runtime_dependency 'microsoft_kiota_abstractions', '>= 0.14', '< 0.16' + spec.add_runtime_dependency 'microsoft_kiota_abstractions', '>= 0.16', '< 0.18' spec.add_runtime_dependency 'uuidtools', '>= 2.2', '< 3.1' spec.add_runtime_dependency 'json', '>= 2.6.3', '< 2.22.0' spec.add_development_dependency 'rake', '~> 13.0' diff --git a/spec/json_serialization_writer_composed_type_spec.rb b/spec/json_serialization_writer_composed_type_spec.rb index 5d3d742..9bffd43 100644 --- a/spec/json_serialization_writer_composed_type_spec.rb +++ b/spec/json_serialization_writer_composed_type_spec.rb @@ -3,16 +3,6 @@ require_relative 'spec_helper' require 'microsoft_kiota_abstractions' -module MicrosoftKiotaAbstractions - module ComposedTypeWrapper; end - - module ParseNodeHelper - def self.merge_deserializers_for_intersection_wrapper(*parsables) - parsables.compact.reduce({}) { |acc, p| acc.merge(p.get_field_deserializers) } - end - end -end unless defined?(MicrosoftKiotaAbstractions::ComposedTypeWrapper) - module TestModels class SimpleParsable include MicrosoftKiotaAbstractions::Parsable