From a66f03e408cadb756c49abfa68b3886560479dec Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Sun, 11 Jan 2026 22:59:49 +0200 Subject: [PATCH 1/5] reproduce the issue relates to gh-2402 Signed-off-by: Emanuel Trandafir --- spring-cloud-contract-verifier/pom.xml | 6 + .../ContractVerifierObjectMapperSpec.groovy | 9 + .../verifier/messaging/internal/FooAvro.java | 351 ++++++++++++++++++ 3 files changed, 366 insertions(+) create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 1c90d4e529..604af938cc 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -280,6 +280,12 @@ spring-boot-resttestclient test + + org.apache.avro + avro + 1.12.1 + test + diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy index 6eef58106f..0eb99621d4 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy @@ -32,6 +32,15 @@ class ContractVerifierObjectMapperSpec extends Specification { result == '''{"foo":"bar"}''' } + def "should convert an Avro-generated object into a json representation"() { + given: + FooAvro input = FooAvro.newBuilder().setFooAvro("barAvro").build() + when: + String result = mapper.writeValueAsString(input) + then: + result == '''{"fooAvro":"barAvro"}''' + } + def "should convert bytes into a json representation"() { given: String input = '''{"foo":"bar"}''' diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java new file mode 100644 index 0000000000..1115ae8767 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java @@ -0,0 +1,351 @@ +/** + * Autogenerated by Avro + * + * DO NOT EDIT DIRECTLY + */ +package org.springframework.cloud.contract.verifier.messaging.internal; + +import org.apache.avro.message.BinaryMessageDecoder; +import org.apache.avro.message.BinaryMessageEncoder; +import org.apache.avro.message.SchemaStore; +import org.apache.avro.specific.SpecificData; + +/** Dummy Avro object for testing purposes */ +@org.apache.avro.specific.AvroGenerated +public class FooAvro extends org.apache.avro.specific.SpecificRecordBase + implements org.apache.avro.specific.SpecificRecord { + private static final long serialVersionUID = -2221379489582530192L; + + public static final org.apache.avro.Schema SCHEMA$ = new org.apache.avro.Schema.Parser().parse( + "{\"type\":\"record\",\"name\":\"FooAvro\",\"namespace\":\"org.springframework.cloud.contract.verifier.messaging.internal\",\"doc\":\"Dummy Avro object for testing purposes\",\"fields\":[{\"name\":\"fooAvro\",\"type\":{\"type\":\"string\",\"avro.java.string\":\"String\"},\"doc\":\"foo field\"}]}"); + + public static org.apache.avro.Schema getClassSchema() { + return SCHEMA$; + } + + private static final SpecificData MODEL$ = new SpecificData(); + + private static final BinaryMessageEncoder ENCODER = new BinaryMessageEncoder<>( + MODEL$, SCHEMA$); + + private static final BinaryMessageDecoder DECODER = new BinaryMessageDecoder<>( + MODEL$, SCHEMA$); + + /** + * Return the BinaryMessageEncoder instance used by this class. + * @return the message encoder used by this class + */ + public static BinaryMessageEncoder getEncoder() { + return ENCODER; + } + + /** + * Return the BinaryMessageDecoder instance used by this class. + * @return the message decoder used by this class + */ + public static BinaryMessageDecoder getDecoder() { + return DECODER; + } + + /** + * Create a new BinaryMessageDecoder instance for this class that uses the specified + * {@link SchemaStore}. + * @param resolver a {@link SchemaStore} used to find schemas by fingerprint + * @return a BinaryMessageDecoder instance for this class backed by the given + * SchemaStore + */ + public static BinaryMessageDecoder createDecoder(SchemaStore resolver) { + return new BinaryMessageDecoder<>(MODEL$, SCHEMA$, resolver); + } + + /** + * Serializes this FooAvro to a ByteBuffer. + * @return a buffer holding the serialized data for this instance + * @throws java.io.IOException if this instance could not be serialized + */ + public java.nio.ByteBuffer toByteBuffer() throws java.io.IOException { + return ENCODER.encode(this); + } + + /** + * Deserializes a FooAvro from a ByteBuffer. + * @param b a byte buffer holding serialized data for an instance of this class + * @return a FooAvro instance decoded from the given buffer + * @throws java.io.IOException if the given bytes could not be deserialized into an + * instance of this class + */ + public static FooAvro fromByteBuffer(java.nio.ByteBuffer b) + throws java.io.IOException { + return DECODER.decode(b); + } + + /** foo field */ + private java.lang.String fooAvro; + + /** + * Default constructor. Note that this does not initialize fields to their default + * values from the schema. If that is desired then one should use + * newBuilder(). + */ + public FooAvro() { + } + + /** + * All-args constructor. + * @param fooAvro foo field + */ + public FooAvro(java.lang.String fooAvro) { + this.fooAvro = fooAvro; + } + + @Override + public org.apache.avro.specific.SpecificData getSpecificData() { + return MODEL$; + } + + @Override + public org.apache.avro.Schema getSchema() { + return SCHEMA$; + } + + // Used by DatumWriter. Applications should not call. + @Override + public java.lang.Object get(int field$) { + switch (field$) { + case 0: + return fooAvro; + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + // Used by DatumReader. Applications should not call. + @Override + @SuppressWarnings(value = "unchecked") + public void put(int field$, java.lang.Object value$) { + switch (field$) { + case 0: + fooAvro = value$ != null ? value$.toString() : null; + break; + default: + throw new IndexOutOfBoundsException("Invalid index: " + field$); + } + } + + /** + * Gets the value of the 'fooAvro' field. + * @return foo field + */ + public java.lang.String getFooAvro() { + return fooAvro; + } + + /** + * Sets the value of the 'fooAvro' field. foo field + * @param value the value to set. + */ + public void setFooAvro(java.lang.String value) { + this.fooAvro = value; + } + + /** + * Creates a new FooAvro RecordBuilder. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder() { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + + /** + * Creates a new FooAvro RecordBuilder by copying an existing Builder. + * @param other The existing builder to copy. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder other) { + if (other == null) { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + else { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder( + other); + } + } + + /** + * Creates a new FooAvro RecordBuilder by copying an existing FooAvro instance. + * @param other The existing instance to copy. + * @return A new FooAvro RecordBuilder + */ + public static org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder newBuilder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro other) { + if (other == null) { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder(); + } + else { + return new org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder( + other); + } + } + + /** + * RecordBuilder for FooAvro instances. + */ + @org.apache.avro.specific.AvroGenerated + public static class Builder + extends org.apache.avro.specific.SpecificRecordBuilderBase + implements org.apache.avro.data.RecordBuilder { + + /** foo field */ + private java.lang.String fooAvro; + + /** Creates a new Builder */ + private Builder() { + super(SCHEMA$, MODEL$); + } + + /** + * Creates a Builder by copying an existing Builder. + * @param other The existing Builder to copy. + */ + private Builder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder other) { + super(other); + if (isValidValue(fields()[0], other.fooAvro)) { + this.fooAvro = data().deepCopy(fields()[0].schema(), other.fooAvro); + fieldSetFlags()[0] = other.fieldSetFlags()[0]; + } + } + + /** + * Creates a Builder by copying an existing FooAvro instance + * @param other The existing instance to copy. + */ + private Builder( + org.springframework.cloud.contract.verifier.messaging.internal.FooAvro other) { + super(SCHEMA$, MODEL$); + if (isValidValue(fields()[0], other.fooAvro)) { + this.fooAvro = data().deepCopy(fields()[0].schema(), other.fooAvro); + fieldSetFlags()[0] = true; + } + } + + /** + * Gets the value of the 'fooAvro' field. foo field + * @return The value. + */ + public java.lang.String getFooAvro() { + return fooAvro; + } + + /** + * Sets the value of the 'fooAvro' field. foo field + * @param value The value of 'fooAvro'. + * @return This builder. + */ + public org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder setFooAvro( + java.lang.String value) { + validate(fields()[0], value); + this.fooAvro = value; + fieldSetFlags()[0] = true; + return this; + } + + /** + * Checks whether the 'fooAvro' field has been set. foo field + * @return True if the 'fooAvro' field has been set, false otherwise. + */ + public boolean hasFooAvro() { + return fieldSetFlags()[0]; + } + + /** + * Clears the value of the 'fooAvro' field. foo field + * @return This builder. + */ + public org.springframework.cloud.contract.verifier.messaging.internal.FooAvro.Builder clearFooAvro() { + fooAvro = null; + fieldSetFlags()[0] = false; + return this; + } + + @Override + @SuppressWarnings("unchecked") + public FooAvro build() { + try { + FooAvro record = new FooAvro(); + record.fooAvro = fieldSetFlags()[0] ? + this.fooAvro : + (java.lang.String) defaultValue(fields()[0]); + return record; + } + catch (org.apache.avro.AvroMissingFieldException e) { + throw e; + } + catch (java.lang.Exception e) { + throw new org.apache.avro.AvroRuntimeException(e); + } + } + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumWriter WRITER$ = (org.apache.avro.io.DatumWriter) MODEL$.createDatumWriter( + SCHEMA$); + + @Override + public void writeExternal(java.io.ObjectOutput out) throws java.io.IOException { + WRITER$.write(this, SpecificData.getEncoder(out)); + } + + @SuppressWarnings("unchecked") + private static final org.apache.avro.io.DatumReader READER$ = (org.apache.avro.io.DatumReader) MODEL$.createDatumReader( + SCHEMA$); + + @Override + public void readExternal(java.io.ObjectInput in) throws java.io.IOException { + READER$.read(this, SpecificData.getDecoder(in)); + } + + @Override + protected boolean hasCustomCoders() { + return true; + } + + @Override + public void customEncode(org.apache.avro.io.Encoder out) throws java.io.IOException { + out.writeString(this.fooAvro); + + } + + @Override + public void customDecode(org.apache.avro.io.ResolvingDecoder in) + throws java.io.IOException { + org.apache.avro.Schema.Field[] fieldOrder = in.readFieldOrderIfDiff(); + if (fieldOrder == null) { + this.fooAvro = in.readString(); + + } + else { + for (int i = 0; i < 1; i++) { + switch (fieldOrder[i].pos()) { + case 0: + this.fooAvro = in.readString(); + break; + + default: + throw new java.io.IOException("Corrupt ResolvingDecoder."); + } + } + } + } +} + + + + + + + + + + From 558624b85a03d03d8dc65a574774bdd783ef8d44 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Sat, 17 Jan 2026 14:17:13 +0200 Subject: [PATCH 2/5] ignore avro-specific fields if avro is in the classpath closes gh-2402 Signed-off-by: Emanuel Trandafir --- spring-cloud-contract-verifier/pom.xml | 6 ++++ .../ContractVerifierObjectMapper.java | 32 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 604af938cc..89c89abd5a 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -286,6 +286,12 @@ 1.12.1 test + + org.apache.avro + avro + 1.12.1 + provided + diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java index 707754a7f6..c552f986d1 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java @@ -16,24 +16,27 @@ package org.springframework.cloud.contract.verifier.messaging.internal; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + import tools.jackson.databind.json.JsonMapper; /** * Wrapper over {@link JsonMapper} that won't try to parse String but will directly return * it. - * * @author Marcin Grzejszczak */ public class ContractVerifierObjectMapper { private final JsonMapper objectMapper; - public ContractVerifierObjectMapper(JsonMapper objectMapper) { - this.objectMapper = objectMapper; + public ContractVerifierObjectMapper() { + this(new JsonMapper()); } - public ContractVerifierObjectMapper() { - this.objectMapper = new JsonMapper(); + public ContractVerifierObjectMapper(JsonMapper mapper) { + this.objectMapper = usesAvro() ? ignoreAvroFields(mapper) : mapper; } public String writeValueAsString(Object payload) { @@ -56,4 +59,23 @@ else if (payload instanceof byte[]) { return this.objectMapper.writeValueAsBytes(payload); } + private static boolean usesAvro() { + return ClassUtils.isPresent("org.apache.avro.specific.SpecificRecordBase", null); + } + + private static JsonMapper ignoreAvroFields(JsonMapper mapper) { + try { + return mapper.rebuild().addMixIn( + ClassUtils.forName("org.apache.avro.specific.SpecificRecordBase", + null), IgnoreAvroMixin.class).build(); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) + interface IgnoreAvroMixin { + } + } From a74018fd690176ad4be65efd1bb7747edaa24065 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Sat, 17 Jan 2026 16:24:53 +0200 Subject: [PATCH 3/5] code review closes gh-2402 Signed-off-by: Emanuel Trandafir --- spring-cloud-contract-verifier/pom.xml | 10 ++++- .../ContractVerifierObjectMapper.java | 32 +++------------- ...NoOpContractVerifierAutoConfiguration.java | 30 ++++++++++++--- ...ontractVerifierObjectMapperAvroSpec.groovy | 38 +++++++++++++++++++ .../ContractVerifierObjectMapperSpec.groovy | 9 ----- .../verifier/messaging/internal/FooAvro.java | 12 +----- 6 files changed, 76 insertions(+), 55 deletions(-) create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml index 89c89abd5a..0b9e84c1db 100644 --- a/spring-cloud-contract-verifier/pom.xml +++ b/spring-cloud-contract-verifier/pom.xml @@ -5,6 +5,7 @@ 4.0.0 2.1.1 + 1.12.1 org.springframework.cloud @@ -275,6 +276,11 @@ spock-junit4 test + + org.spockframework + spock-spring + test + org.springframework.boot spring-boot-resttestclient @@ -283,13 +289,13 @@ org.apache.avro avro - 1.12.1 + ${avro.version} test org.apache.avro avro - 1.12.1 + ${avro.version} provided diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java index c552f986d1..707754a7f6 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapper.java @@ -16,27 +16,24 @@ package org.springframework.cloud.contract.verifier.messaging.internal; -import org.springframework.util.ClassUtils; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - import tools.jackson.databind.json.JsonMapper; /** * Wrapper over {@link JsonMapper} that won't try to parse String but will directly return * it. + * * @author Marcin Grzejszczak */ public class ContractVerifierObjectMapper { private final JsonMapper objectMapper; - public ContractVerifierObjectMapper() { - this(new JsonMapper()); + public ContractVerifierObjectMapper(JsonMapper objectMapper) { + this.objectMapper = objectMapper; } - public ContractVerifierObjectMapper(JsonMapper mapper) { - this.objectMapper = usesAvro() ? ignoreAvroFields(mapper) : mapper; + public ContractVerifierObjectMapper() { + this.objectMapper = new JsonMapper(); } public String writeValueAsString(Object payload) { @@ -59,23 +56,4 @@ else if (payload instanceof byte[]) { return this.objectMapper.writeValueAsBytes(payload); } - private static boolean usesAvro() { - return ClassUtils.isPresent("org.apache.avro.specific.SpecificRecordBase", null); - } - - private static JsonMapper ignoreAvroFields(JsonMapper mapper) { - try { - return mapper.rebuild().addMixIn( - ClassUtils.forName("org.apache.avro.specific.SpecificRecordBase", - null), IgnoreAvroMixin.class).build(); - } - catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) - interface IgnoreAvroMixin { - } - } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java index 1a562bb36d..f3f75b70e0 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java @@ -19,12 +19,15 @@ import java.util.Map; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.jspecify.annotations.Nullable; import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.cloud.contract.verifier.converter.YamlContract; import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver; import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender; @@ -89,12 +92,27 @@ public ContractVerifierMessaging contractVerifierMessaging() { @Bean @ConditionalOnMissingBean - public ContractVerifierObjectMapper contractVerifierObjectMapper(ObjectProvider jsonMapper) { - JsonMapper mapper = jsonMapper.getIfAvailable(); - if (mapper != null) { - return new ContractVerifierObjectMapper(mapper); - } - return new ContractVerifierObjectMapper(); + @ConditionalOnMissingClass("org.apache.avro.specific.SpecificRecordBase") + public ContractVerifierObjectMapper contractVerifierObjectMapper( + ObjectProvider jsonMapper) { + JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new); + return new ContractVerifierObjectMapper(mapper); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnClass(name = "org.apache.avro.specific.SpecificRecordBase") + public ContractVerifierObjectMapper avroContractVerifierObjectMapper( + ObjectProvider jsonMapper) throws ClassNotFoundException { + JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new).rebuild() + .addMixIn(Class.forName("org.apache.avro.specific.SpecificRecordBase"), + IgnoreAvroMixin.class).build(); + return new ContractVerifierObjectMapper(mapper); + } + + @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) + interface IgnoreAvroMixin { + } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy new file mode 100644 index 0000000000..e2b223a3f6 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperAvroSpec.groovy @@ -0,0 +1,38 @@ +/* + * Copyright 2013-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.contract.verifier.messaging.internal + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +@ContextConfiguration(classes = [NoOpContractVerifierAutoConfiguration]) +class ContractVerifierObjectMapperAvroSpec extends Specification { + + @Autowired + ContractVerifierObjectMapper mapper + + def "should convert an Avro-generated object into a json representation"() { + given: + FooAvro input = FooAvro.newBuilder().setFooAvro("barAvro").build() + when: + String result = mapper.writeValueAsString(input) + then: + result == '{"fooAvro":"barAvro"}' + } +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy index 0eb99621d4..6eef58106f 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/ContractVerifierObjectMapperSpec.groovy @@ -32,15 +32,6 @@ class ContractVerifierObjectMapperSpec extends Specification { result == '''{"foo":"bar"}''' } - def "should convert an Avro-generated object into a json representation"() { - given: - FooAvro input = FooAvro.newBuilder().setFooAvro("barAvro").build() - when: - String result = mapper.writeValueAsString(input) - then: - result == '''{"fooAvro":"barAvro"}''' - } - def "should convert bytes into a json representation"() { given: String input = '''{"foo":"bar"}''' diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java index 1115ae8767..1b9e9b0b96 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/messaging/internal/FooAvro.java @@ -338,14 +338,4 @@ public void customDecode(org.apache.avro.io.ResolvingDecoder in) } } } -} - - - - - - - - - - +} \ No newline at end of file From bea662ff90952c113589cc1e906e009808685492 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Wed, 21 Jan 2026 20:31:45 +0200 Subject: [PATCH 4/5] code review closes gh-2402 Signed-off-by: Emanuel Trandafir --- ...NoOpContractVerifierAutoConfiguration.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java index f3f75b70e0..6341ff745f 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java @@ -99,20 +99,25 @@ public ContractVerifierObjectMapper contractVerifierObjectMapper( return new ContractVerifierObjectMapper(mapper); } - @Bean - @ConditionalOnMissingBean + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "org.apache.avro.specific.SpecificRecordBase") - public ContractVerifierObjectMapper avroContractVerifierObjectMapper( - ObjectProvider jsonMapper) throws ClassNotFoundException { - JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new).rebuild() - .addMixIn(Class.forName("org.apache.avro.specific.SpecificRecordBase"), - IgnoreAvroMixin.class).build(); - return new ContractVerifierObjectMapper(mapper); - } - - @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) - interface IgnoreAvroMixin { - + public static class AvroContractVerifierObjectMapperConfiguration { + + @Bean + @ConditionalOnMissingBean + public ContractVerifierObjectMapper avroContractVerifierObjectMapper( + ObjectProvider jsonMapper) throws ClassNotFoundException { + JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new).rebuild() + .addMixIn( + Class.forName("org.apache.avro.specific.SpecificRecordBase"), + IgnoreAvroMixin.class).build(); + return new ContractVerifierObjectMapper(mapper); + } + + @JsonIgnoreProperties({ "schema", "specificData", "classSchema", "conversion" }) + interface IgnoreAvroMixin { + + } } } From db8f8686a8e5ea1c841d124c1f5f1e28fdd01458 Mon Sep 17 00:00:00 2001 From: Emanuel Trandafir Date: Thu, 22 Jan 2026 16:19:03 +0200 Subject: [PATCH 5/5] remove reflection code closes gh-2402 Signed-off-by: Emanuel Trandafir --- .../noop/NoOpContractVerifierAutoConfiguration.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java index 6341ff745f..8eb1bd7081 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/noop/NoOpContractVerifierAutoConfiguration.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.apache.avro.specific.SpecificRecordBase; import org.jspecify.annotations.Nullable; import tools.jackson.databind.json.JsonMapper; @@ -100,7 +101,7 @@ public ContractVerifierObjectMapper contractVerifierObjectMapper( } @Configuration(proxyBeanMethods = false) - @ConditionalOnClass(name = "org.apache.avro.specific.SpecificRecordBase") + @ConditionalOnClass(SpecificRecordBase.class) public static class AvroContractVerifierObjectMapperConfiguration { @Bean @@ -108,9 +109,7 @@ public static class AvroContractVerifierObjectMapperConfiguration { public ContractVerifierObjectMapper avroContractVerifierObjectMapper( ObjectProvider jsonMapper) throws ClassNotFoundException { JsonMapper mapper = jsonMapper.getIfAvailable(JsonMapper::new).rebuild() - .addMixIn( - Class.forName("org.apache.avro.specific.SpecificRecordBase"), - IgnoreAvroMixin.class).build(); + .addMixIn(SpecificRecordBase.class, IgnoreAvroMixin.class).build(); return new ContractVerifierObjectMapper(mapper); }