diff --git a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy index b00a5806a..610831c46 100644 --- a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy +++ b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonPropertySpec.groovy @@ -1,10 +1,9 @@ package io.micronaut.serde.jackson -import io.micronaut.core.beans.exceptions.IntrospectionException -import spock.lang.PendingFeature + import spock.lang.Unroll -abstract class JsonPropertySpec extends JsonCompileSpec { +class JsonPropertySpec extends JsonCompileSpec { void "test JsonProperty on private methods"() { when: @@ -125,42 +124,6 @@ class Test { URL | 'ws://junk' } - @PendingFeature - void "test required primitive field"() { - given: - def ctx = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonProperty(required = true) - private int value; - - @JsonCreator - Test(@JsonProperty("value") int value) { - this.value = value; - } - - public int getValue() { - return value; - } -} -""") - - when: - def bean = jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) - then: - def e = thrown(Exception) - e.message.contains("Unable to deserialize type [test.Test]. Required constructor parameter [int value] at index [0] is not present or is null in the supplied data") - - cleanup: - ctx.close() - } - void "test optional by default primitive field"() { given: @@ -218,8 +181,7 @@ class Test { """) when: - def bean = - jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) + def bean = jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) then: bean.value == value @@ -272,135 +234,4 @@ record Test( URL | 'ws://junk' } - @PendingFeature - void "test @JsonProperty on field"() { - given: - def context = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonProperty(value = "other", defaultValue = "default") - private String value; - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private boolean ignored; - public void setValue(String value) { - this.value = value; - } - public String getValue() { - return value; - } - - public void setIgnored(boolean b) { - this.ignored = b; - } - - public boolean isIgnored() { - return ignored; - } -} -""", [value: 'test']) - when: - def result = writeJson(jsonMapper, beanUnderTest) - - then: - result == '{"other":"test"}' - - when: - def bean = jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - then: - bean.ignored == true - bean.value == 'default' - - cleanup: - context.close() - - } - - @PendingFeature - void "test @JsonProperty records"() { - given: - def context = buildContext(""" -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -record Test( - @JsonProperty(value = "other", defaultValue = "default") - String value, - @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "false") - boolean ignored -) {} -""") - when: - def bean = newInstance(context, 'test.Test', "test", false) - def result = writeJson(jsonMapper, bean) - - then: - result == '{"other":"test"}' - - when: - bean = jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - - then: - bean.ignored == true - bean.value == 'default' - - when: - bean = jsonMapper.readValue('{}', argumentOf(context, 'test.Test')) - - then: - bean.ignored == false - bean.value == 'default' - - cleanup: - context.close() - - } - - @PendingFeature - void "test @JsonProperty records - invalid default value"() { - given: - def context = buildContext(""" -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable(validate = false) -record Test( - @JsonProperty(value = "other", defaultValue = "default") - String value, - @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "junk") - int number -) {} -""") - when: - def bean = newInstance(context, 'test.Test', "test", 10) - def result = writeJson(jsonMapper, bean) - - then: - result == '{"other":"test"}' - - when: - bean = jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - - then: - def e = thrown(IntrospectionException) - e.cause.message.contains("Constructor Argument [int number] of type [test.Test] defines an invalid default value") - - cleanup: - context.close() - - } - - } diff --git a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonUnwrappedSpec.groovy b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonUnwrappedSpec.groovy index d62d7895f..eb194d034 100644 --- a/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonUnwrappedSpec.groovy +++ b/serde-jackson-tck/src/main/groovy/io/micronaut/serde/jackson/JsonUnwrappedSpec.groovy @@ -1,9 +1,6 @@ package io.micronaut.serde.jackson import io.micronaut.core.type.Argument -import io.micronaut.serde.jackson.nested.Address -import io.micronaut.serde.jackson.nested.NestedEntity -import io.micronaut.serde.jackson.nested.NestedEntityId class JsonUnwrappedSpec extends JsonCompileSpec { @@ -331,7 +328,7 @@ class Name { def result = writeJson(jsonMapper, parent) then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' + result == '{"age":10,"first":"Fred","last":"Flinstone","ssn":"abc-123"}' when: def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) @@ -350,37 +347,42 @@ class Name { read.age == 15 read.name.first == 'Barney' read.name.last == "Rubble" - read.name.ssn == 'def-789' + read.name.ssn == null cleanup: context.close() } - void "test @JsonUnwrapped records"() { + void "test @JsonUnwrapped with writeOnly field"() { given: def context = buildContext(""" package unwrapped; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; import io.micronaut.core.annotation.Introspected; import io.micronaut.serde.annotation.Serdeable; @Serdeable -record Parent( - int age, +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Parent { + public int age; @JsonUnwrapped - Name name) { + public Name name; } @Serdeable -record Name( - String first, String last -) {} +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Name { + public String first, last; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + public String ssn; +} """) when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', 10, name) + def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone", ssn:"abc-123"]) + def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) def result = writeJson(jsonMapper, parent) @@ -394,6 +396,17 @@ record Name( read.age == 10 read.name.first == 'Fred' read.name.last == "Flinstone" + read.name.ssn == null + + when: + def jsonStr = '{"age":15,"first":"Barney","last":"Rubble","ssn":"def-789"}' + read = jsonMapper.readValue(jsonStr, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) + + then: + read.age == 15 + read.name.first == 'Barney' + read.name.last == "Rubble" + read.name.ssn == 'def-789' cleanup: context.close() @@ -551,60 +564,6 @@ class Name { context.close() } - void "test @JsonUnwrapped - parent constructor args"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public final int age; - @JsonUnwrapped - public final Name name; - - Parent(int age, @JsonUnwrapped Name name) { - this.age = age; - this.name = name; - } -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public final String first, last; - Name(String first, String last) { - this.first = first; - this.last = last; - } -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', 10, name) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - void "test @JsonUnwrapped - levels"() { given: def context = buildContext(""" @@ -709,197 +668,211 @@ class InnerFooId { context.close() } - void "test @JsonUnwrapped - levels 2"() { + // TODO: Correct properties order + void "test @JsonUnwrapped - levels 2"() { given: - def ctx = buildContext("") + def ctx = buildContext(""" +package unwrapped; - when: - def nestedEntity = new NestedEntity(); - nestedEntity.setValue("test1"); - NestedEntityId hashKey = new NestedEntityId(); - hashKey.setTheInt(100); - hashKey.setTheString("MyString"); - nestedEntity.setHashKey(hashKey); - Address address = new Address(); - address.getCityData().setCity("NY"); - address.getCityData().setZipCode("22000"); - address.setStreet("Blvd 11"); - nestedEntity.setAddress(address); - def nestedJsonStr = writeJson(jsonMapper, nestedEntity) +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.serde.annotation.Serdeable; +import java.sql.Timestamp; +import java.util.Date; +import io.micronaut.core.annotation.NonNull; - then: - nestedJsonStr == '{"hk_theInt":100,"hk_theString":"MyString","value":"test1","addr_street":"Blvd 11","addr_cd_zipCode":"22000","addr_cd_city":"NY","version":1,"dateCreated":"1970-01-01T00:00:00Z","dateUpdated":"1970-01-01T00:00:00Z"}' +@Serdeable +class NestedEntity { - when: - def deserNestedEntity = jsonMapper.readValue(nestedJsonStr, NestedEntity.class) + @JsonUnwrapped(prefix = "hk_") + private NestedEntityId hashKey; - then: - deserNestedEntity - deserNestedEntity.hashKey.theInt == nestedEntity.hashKey.theInt - deserNestedEntity.value == nestedEntity.value - deserNestedEntity.audit.dateCreated == nestedEntity.audit.dateCreated - deserNestedEntity.address.cityData.zipCode == nestedEntity.address.cityData.zipCode - deserNestedEntity.address.street == nestedEntity.address.street + private String value; - cleanup: - ctx.close() + @JsonUnwrapped(prefix = "addr_") + private Address address; + + @JsonUnwrapped + private Audit audit = new Audit(); + + public NestedEntityId getHashKey() { + return hashKey; } - void "test @JsonUnwrapped - subtyping"() { - given: - def context = buildContext(""" -package unwrapped; + public void setHashKey(NestedEntityId hashKey) { + this.hashKey = hashKey; + } -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; + public String getValue() { + return value; + } -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Wrapper { - @JsonUnwrapped - public final SuperClass name; + public void setValue(String value) { + this.value = value; + } - Wrapper(@JsonUnwrapped SuperClass name) { - this.name = name; - } -} + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -abstract class SuperClass { - public final String first; - SuperClass(String first) { - this.first = first; - } } @Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class SubClass extends SuperClass { - public final String last; - SubClass(String first, String last) { - super(first); - this.last = last; - } -} -""") - when: - def name = newInstance(context, 'unwrapped.SubClass', "Fred", "Flinstone") - def wrapper = newInstance(context, 'unwrapped.Wrapper', name) +class NestedEntityId { - def result = writeJson(jsonMapper, wrapper) + private Integer theInt; - then: - result == '{"first":"Fred","last":"Flinstone"}' + private String theString; - cleanup: - context.close() + public Integer getTheInt() { + return theInt; } - void 'test wrapped subtype with property info'() { - given: - def context = buildContext('test.Base', """ -package test; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; + public void setTheInt(Integer theInt) { + this.theInt = theInt; + } -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Wrapper { - public final String foo; - @JsonUnwrapped - public final Base base; + public String getTheString() { + return theString; + } - Wrapper(String foo, @JsonUnwrapped Base base) { - this.base = base; - this.foo = foo; - } + public void setTheString(String theString) { + this.theString = theString; + } } @Serdeable -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes( - @JsonSubTypes.Type(value = Sub.class, name = "sub-class") -) -class Base { - private String string; +class Address { + + @JsonUnwrapped(prefix = "cd_") + private CityData cityData = new CityData(); - public Base(String string) { - this.string = string; + private String street; + + public String getStreet() { + return street; } - public String getString() { - return string; + public void setStreet(String street) { + this.street = street; + } + + public CityData getCityData() { + return cityData; + } + + public void setCityData(CityData cityData) { + this.cityData = cityData; } } @Serdeable -class Sub extends Base { - private Integer integer; +class Audit { + + static final Timestamp MIN_TIMESTAMP = new Timestamp(new Date(0).getTime()); + + private Long version = 1L; + + // Init manually because cannot be nullable and not getting populated by the event + private Timestamp dateCreated = MIN_TIMESTAMP; + + private Timestamp dateUpdated = MIN_TIMESTAMP; + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Timestamp getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Timestamp dateCreated) { + this.dateCreated = dateCreated; + } - public Sub(String string, Integer integer) { - super(string); - this.integer = integer; + public Timestamp getDateUpdated() { + return dateUpdated; } - public Integer getInteger() { - return integer; + public void setDateUpdated(Timestamp dateUpdated) { + this.dateUpdated = dateUpdated; } } -""") - when: - def base = newInstance(context, 'test.Sub', "a", 1) - def wrapper = newInstance(context, 'test.Wrapper', "bar", base) - def result = writeJson(jsonMapper, wrapper) +@Serdeable +class CityData { - then: - result == '{"foo":"bar","type":"sub-class","string":"a","integer":1}' + @NonNull + private String zipCode; - when: - result = jsonMapper.readValue(result, argumentOf(context, "test.Wrapper")) + private String city; - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 + public String getZipCode() { + return zipCode; + } - when: - result = jsonMapper.readValue('{"string":"a","integer":1,"type":"sub-class","foo":"bar"}', argumentOf(context, "test.Wrapper")) + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 + public String getCity() { + return city; + } - when: - result = jsonMapper.readValue('{"foo":"bar", "type":"some-other-type","string":"a","integer":1}', argumentOf(context, "test.Wrapper")) + public void setCity(String city) { + this.city = city; + } +} - then: - result.getClass().name != 'test.Sub' + +""") when: - result = jsonMapper.readValue('{"string":"a","integer":1,"foo":"bar","type":"Sub"}', argumentOf(context, "test.Wrapper")) + def nestedJsonStr = '{"hk_theInt":100,"hk_theString":"MyString","value":"test1","addr_street":"Blvd 11","addr_cd_zipCode":"22000","addr_cd_city":"NY","version":1,"dateCreated":"1970-01-01T00:00:00Z","dateUpdated":"1970-01-01T00:00:00Z"}' + def deserNestedEntity = jsonMapper.readValue(nestedJsonStr, Argument.of(ctx.classLoader.loadClass('unwrapped.NestedEntity'))) then: - result.getClass().name != 'test.Sub' + deserNestedEntity + deserNestedEntity.hashKey.theInt == 100 + deserNestedEntity.hashKey.theString == "MyString" + deserNestedEntity.value == "test1" + deserNestedEntity.address.cityData.zipCode == "22000" + deserNestedEntity.address.cityData.city == "NY" + deserNestedEntity.address.street == "Blvd 11" + deserNestedEntity.audit.version == 1 + deserNestedEntity.audit.dateCreated + deserNestedEntity.audit.dateUpdated == deserNestedEntity.audit.dateCreated + +// when: +// def result = jsonMapper.writeValueAsString(deserNestedEntity) +// +// then: +// result == nestedJsonStr + + cleanup: + ctx.close() } - void 'test wrapped subtype with wrapper info'() { + void "test @JsonUnwrapped - subtyping"() { given: - def context = buildContext('test.Base', """ -package test; + def context = buildContext(""" +package unwrapped; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonUnwrapped; import io.micronaut.core.annotation.Introspected; import io.micronaut.serde.annotation.Serdeable; @@ -907,69 +880,44 @@ import io.micronaut.serde.annotation.Serdeable; @Serdeable @Introspected(accessKind = Introspected.AccessKind.FIELD) class Wrapper { - public final String foo; @JsonUnwrapped - public final Base base; + public final SuperClass name; - Wrapper(String foo, @JsonUnwrapped Base base) { - this.base = base; - this.foo = foo; + Wrapper(@JsonUnwrapped SuperClass name) { + this.name = name; } } @Serdeable -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) -@JsonSubTypes( - @JsonSubTypes.Type(value = Sub.class, name = "subClass") -) -class Base { - private String string; - - public Base(String string) { - this.string = string; - } - - public String getString() { - return string; - } +@Introspected(accessKind = Introspected.AccessKind.FIELD) +abstract class SuperClass { + public final String first; + SuperClass(String first) { + this.first = first; + } } @Serdeable -class Sub extends Base { - private Integer integer; - - public Sub(String string, Integer integer) { - super(string); - this.integer = integer; - } - - public Integer getInteger() { - return integer; - } +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class SubClass extends SuperClass { + public final String last; + SubClass(String first, String last) { + super(first); + this.last = last; + } } """) when: - def result = jsonMapper.readValue('{"foo":"bar","subClass":{"string":"a","integer":1}}', argumentOf(context, "test.Wrapper")) - - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 + def name = newInstance(context, 'unwrapped.SubClass', "Fred", "Flinstone") + def wrapper = newInstance(context, 'unwrapped.Wrapper', name) - when: - result = jsonMapper.readValue('{"subClass":{"string":"a","integer":1}, "foo":"bar"}', argumentOf(context, "test.Wrapper")) + def result = writeJson(jsonMapper, wrapper) then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 - - when: - def json = writeJson(jsonMapper, result) + result == '{"first":"Fred","last":"Flinstone"}' - then: - json == '{"foo":"bar","subClass":{"string":"a","integer":1}}' + cleanup: + context.close() } + } diff --git a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Address.java b/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Address.java deleted file mode 100644 index f777fa4ba..000000000 --- a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Address.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class Address { - - @JsonUnwrapped(prefix = "cd_") - private CityData cityData = new CityData(); - - private String street; - - public String getStreet() { - return street; - } - - public void setStreet(String street) { - this.street = street; - } - - public CityData getCityData() { - return cityData; - } - - public void setCityData(CityData cityData) { - this.cityData = cityData; - } -} diff --git a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Audit.java b/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Audit.java deleted file mode 100644 index e9831d8ac..000000000 --- a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/Audit.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.serde.annotation.Serdeable; - -import java.sql.Timestamp; -import java.util.Date; - -@Serdeable -public class Audit { - - static final Timestamp MIN_TIMESTAMP = new Timestamp(new Date(0).getTime()); - - private Long version = 1L; - - // Init manually because cannot be nullable and not getting populated by the event - private Timestamp dateCreated = MIN_TIMESTAMP; - - private Timestamp dateUpdated = MIN_TIMESTAMP; - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - public Timestamp getDateCreated() { - return dateCreated; - } - - public void setDateCreated(Timestamp dateCreated) { - this.dateCreated = dateCreated; - } - - public Timestamp getDateUpdated() { - return dateUpdated; - } - - public void setDateUpdated(Timestamp dateUpdated) { - this.dateUpdated = dateUpdated; - } -} diff --git a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/CityData.java b/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/CityData.java deleted file mode 100644 index bc710a552..000000000 --- a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/CityData.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class CityData { - - @NonNull - private String zipCode; - - private String city; - - public String getZipCode() { - return zipCode; - } - - public void setZipCode(String zipCode) { - this.zipCode = zipCode; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } -} diff --git a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntity.java b/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntity.java deleted file mode 100644 index b02d286d8..000000000 --- a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntity.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class NestedEntity { - - @JsonUnwrapped(prefix = "hk_") - private NestedEntityId hashKey; - - private String value; - - @JsonUnwrapped(prefix = "addr_") - private Address address; - - @JsonUnwrapped - private Audit audit = new Audit(); - - public NestedEntityId getHashKey() { - return hashKey; - } - - public void setHashKey(NestedEntityId hashKey) { - this.hashKey = hashKey; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public Address getAddress() { - return address; - } - - public void setAddress(Address address) { - this.address = address; - } - - public Audit getAudit() { - return audit; - } - - public void setAudit(Audit audit) { - this.audit = audit; - } - -} diff --git a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntityId.java b/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntityId.java deleted file mode 100644 index 9ebf909b4..000000000 --- a/serde-jackson-tck/src/main/java/io/micronaut/serde/jackson/nested/NestedEntityId.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class NestedEntityId { - - private Integer theInt; - - private String theString; - - public Integer getTheInt() { - return theInt; - } - - public void setTheInt(Integer theInt) { - this.theInt = theInt; - } - - public String getTheString() { - return theString; - } - - public void setTheString(String theString) { - this.theString = theString; - } -} diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonPropertySpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonPropertySpec.groovy deleted file mode 100644 index 25b8a4812..000000000 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonPropertySpec.groovy +++ /dev/null @@ -1,403 +0,0 @@ -package io.micronaut.serde.jackson.annotation - -import io.micronaut.core.beans.exceptions.IntrospectionException -import io.micronaut.serde.exceptions.SerdeException -import io.micronaut.serde.jackson.JsonCompileSpec -import spock.lang.Unroll - -class JsonPropertySpec extends JsonCompileSpec { - void "test JsonProperty on private methods"() { - when: - buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonIgnore - private String value; - - public void setValue(String value) { - this.value = value; - } - public String getValue() { - return value; - } - - @JsonProperty("value") - private void setValueInternal(String value) { - this.value = value.toLowerCase(); - } - - @JsonProperty("value") - private String getValueInternal() { - return value.toUpperCase(); - } -} -""", [value: 'test']) - then: - def e = thrown(RuntimeException) - e.message.contains("JSON annotations cannot be used on private methods") - } - - void "test JsonProperty on protected methods"() { - given: - def context = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonIgnore - private String value; - - public void setValue(String value) { - this.value = value; - } - public String getValue() { - return value; - } - - @JsonProperty("value") - protected void setValueInternal(String value) { - this.value = value.toLowerCase(); - } - - @JsonProperty("value") - protected String getValueInternal() { - return value.toUpperCase(); - } -} -""", [value: 'test']) - when: - def result = writeJson(jsonMapper, beanUnderTest) - - then: - result == '{"value":"TEST"}' - - when: - def bean = - jsonMapper.readValue(result, argumentOf(context, 'test.Test')) - then: - bean.value == 'test' - - cleanup: - context.close() - } - - @Unroll - void "test invalid defaultValue for #type and value #value"() { - - when: - buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonProperty(defaultValue = "$value") - private $type.name value; - public void setValue($type.name value) { - this.value = value; - } - public $type.name getValue() { - return value; - } -} -""", [:]) - - then: - def e = thrown(RuntimeException) - e.message.contains("Invalid defaultValue [$value] specified") - - where: - type | value - Integer | 'junk' - URL | 'ws://junk' - } - - void "test required primitive field"() { - - given: - def ctx = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonProperty(required = true) - private int value; - - Test(int value) { - this.value = value; - } - - public int getValue() { - return value; - } -} -""") - - when: - def bean = - jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) - then: - def e = thrown(SerdeException) - e.message.contains("Unable to deserialize type [test.Test]. Required constructor parameter [int value] at index [0] is not present or is null in the supplied data") - - cleanup: - ctx.close() - } - - void "test optional by default primitive field"() { - - given: - def ctx = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - private int value = 5; - - public void setValue(int value) { - this.value = value; - } - public int getValue() { - return value; - } -} -""") - - when: - def bean = - jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) - then: - bean.value == 5 - - cleanup: - ctx.close() - } - - void "test optional by default primitive field in constructor"() { - - given: - def ctx = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; -import io.micronaut.core.annotation.Nullable; - -@Serdeable -class Test { - private final $type value; - - Test($type value) { - this.value = value; - } - - public $type getValue() { - return value; - } -} -""") - - when: - def bean = - jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) - then: - bean.value == value - - cleanup: - ctx.close() - - where: - type | value - "byte" | (byte) 0 - "short" | (short) 0 - "int" | 0 - "long" | 0L - "float" | 0F - "double" | 0D - - "@Nullable Byte" | null - "@Nullable Short" | null - "@Nullable Integer" | null - "@Nullable Long" | null - "@Nullable Float" | null - "@Nullable Double" | null - } - - @Unroll - void "test invalid defaultValue for #type and value #value for records"() { - - when: - buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -record Test( - @JsonProperty(defaultValue = "$value") - $type.name value -) {} -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Invalid defaultValue [$value] specified") - - where: - type | value - Integer | 'junk' - int.class | 'junk' - URL | 'ws://junk' - } - - void "test @JsonProperty on field"() { - given: - def context = buildContext('test.Test', """ -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Test { - @JsonProperty(value = "other", defaultValue = "default") - private String value; - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - private boolean ignored; - public void setValue(String value) { - this.value = value; - } - public String getValue() { - return value; - } - - public void setIgnored(boolean b) { - this.ignored = b; - } - - public boolean isIgnored() { - return ignored; - } -} -""", [value: 'test']) - when: - def result = writeJson(jsonMapper, beanUnderTest) - - then: - result == '{"other":"test"}' - - when: - def bean = - jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - then: - bean.ignored == true - bean.value == 'default' - - cleanup: - context.close() - - } - - void "test @JsonProperty records"() { - given: - def context = buildContext(""" -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -record Test( - @JsonProperty(value = "other", defaultValue = "default") - String value, - @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "false") - boolean ignored -) {} -""") - when: - def bean = newInstance(context, 'test.Test', "test", false) - def result = writeJson(jsonMapper, bean) - - then: - result == '{"other":"test"}' - - when: - bean = jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - - then: - bean.ignored == true - bean.value == 'default' - - when: - bean = jsonMapper.readValue('{}', argumentOf(context, 'test.Test')) - - then: - bean.ignored == false - bean.value == 'default' - - cleanup: - context.close() - - } - - void "test @JsonProperty records - invalid default value"() { - given: - def context = buildContext(""" -package test; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable(validate = false) -record Test( - @JsonProperty(value = "other", defaultValue = "default") - String value, - @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "junk") - int number -) {} -""") - when: - def bean = newInstance(context, 'test.Test', "test", 10) - def result = writeJson(jsonMapper, bean) - - then: - result == '{"other":"test"}' - - when: - bean = jsonMapper.readValue('{"ignored":true}', argumentOf(context, 'test.Test')) - - then: - def e = thrown(IntrospectionException) - e.cause.message.contains("Constructor Argument [int number] of type [test.Test] defines an invalid default value") - - cleanup: - context.close() - - } - - -} diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy deleted file mode 100644 index 6331ec861..000000000 --- a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/JsonUnwrappedSpec.groovy +++ /dev/null @@ -1,967 +0,0 @@ -package io.micronaut.serde.jackson.annotation - -import io.micronaut.core.type.Argument -import io.micronaut.serde.jackson.JsonCompileSpec -import io.micronaut.serde.jackson.nested.Address -import io.micronaut.serde.jackson.nested.NestedEntity -import io.micronaut.serde.jackson.nested.NestedEntityId - -class JsonUnwrappedSpec extends JsonCompileSpec { - - void "test @JsonUnwrapped conflict"() { - when: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - public String first; - @JsonUnwrapped - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; -} -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Unwrapped property contains a property [first] that conflicts with an existing property of the outer type: unwrapped.Parent") - } - - void "test @JsonUnwrapped conflict methods"() { - when: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Parent { - private String first; - private Name name; - public void setFirst(String first) { - this.first = first; - } - public String getFirst() { - return first; - } - @JsonUnwrapped - public void setName(unwrapped.Name name) { - this.name = name; - } - @JsonUnwrapped - public unwrapped.Name getName() { - return name; - } -} - -@Serdeable -class Name { - private String first; - public void setFirst(String first) { - this.first = first; - } - public String getFirst() { - return first; - } -} -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Unwrapped property contains a property [first] that conflicts with an existing property of the outer type: unwrapped.Parent") - } - - void "test @JsonUnwrapped conflict records"() { - when: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -record Parent ( - String first, - @JsonUnwrapped - Name name -) {} - -@Serdeable -record Name (String first) {} -""") - - then: - def e = thrown(RuntimeException) - e.message.contains("Unwrapped property contains a property [first] that conflicts with an existing property of the outer type: unwrapped.Parent") - } - - void "test @JsonUnwrapped"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone"]) - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped with @JsonIgnoreProperties"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped - @JsonIgnoreProperties("ignored") - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; - public String ignored; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone", ignored:"Ignored"]) - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped with @JsonIgnoreProperties and colliding properties"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public String ignored; - @JsonUnwrapped - @JsonIgnoreProperties("ignored") - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; - public String ignored; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone", ignored:"Ignored"]) - def parent = newInstance(context, 'unwrapped.Parent', [ignored:'foo', name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"ignored":"foo","first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.ignored == 'foo' - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped with @JsonIgnoreProperties on class"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -@JsonIgnoreProperties("ignored") -class Name { - public String first, last; - public String ignored; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone", ignored:"Ignored"]) - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped with readOnly field"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; - @JsonProperty(access = JsonProperty.Access.READ_ONLY) - public String ssn; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone", ssn:"abc-123"]) - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - read.name.ssn == null - - when: - def jsonStr = '{"age":15,"first":"Barney","last":"Rubble","ssn":"def-789"}' - read = jsonMapper.readValue(jsonStr, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 15 - read.name.first == 'Barney' - read.name.last == "Rubble" - read.name.ssn == 'def-789' - - cleanup: - context.close() - } - - void "test @JsonUnwrapped records"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -record Parent( - int age, - @JsonUnwrapped - Name name) { -} - -@Serdeable -record Name( - String first, String last -) {} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', 10, name) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - prefix/suffix"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped(prefix = "n_", suffix = "_x") - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public String first, last; -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', [first:"Fred", last:"Flinstone"]) - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"n_first_x":"Fred","n_last_x":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - constructor args"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public final String first, last; - Name(String first, String last) { - this.first = first; - this.last = last; - } -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - constructor args - prefix/suffix"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public int age; - @JsonUnwrapped(prefix = "n_", suffix = "_x") - public Name name; -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public final String first, last; - Name(String first, String last) { - this.first = first; - this.last = last; - } -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', [age:10, name:name]) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"n_first_x":"Fred","n_last_x":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - parent constructor args"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Parent { - public final int age; - @JsonUnwrapped - public final Name name; - - Parent(int age, @JsonUnwrapped Name name) { - this.age = age; - this.name = name; - } -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Name { - public final String first, last; - Name(String first, String last) { - this.first = first; - this.last = last; - } -} -""") - - when: - def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") - def parent = newInstance(context, 'unwrapped.Parent', 10, name) - - def result = writeJson(jsonMapper, parent) - - then: - result == '{"age":10,"first":"Fred","last":"Flinstone"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) - - then: - read.age == 10 - read.name.first == 'Fred' - read.name.last == "Flinstone" - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - levels"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -class Foo { - - @JsonUnwrapped(prefix = "hk_", suffix = "_out") - private ComplexFooId hashKey; - - private String value; - - public ComplexFooId getHashKey() { - return hashKey; - } - - public void setHashKey(ComplexFooId hashKey) { - this.hashKey = hashKey; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } -} -@Serdeable -class ComplexFooId { - - private Integer theInt; - - @JsonUnwrapped(prefix = "foo_", suffix = "_in") - private InnerFooId nested; - - public Integer getTheInt() { - return theInt; - } - - public void setTheInt(Integer theInt) { - this.theInt = theInt; - } - - public InnerFooId getNested() { - return nested; - } - - public void setNested(InnerFooId nested) { - this.nested = nested; - } -} -@Serdeable -class InnerFooId { - - private Long theLong; - - private String theString; - - public Long getTheLong() { - return theLong; - } - - public void setTheLong(Long theLong) { - this.theLong = theLong; - } - - public String getTheString() { - return theString; - } - - public void setTheString(String theString) { - this.theString = theString; - } -} -""") - - when: - def foo = newInstance(context, 'unwrapped.Foo', [value: "TheValue", hashKey: newInstance(context, 'unwrapped.ComplexFooId', [theInt: 10, - nested: newInstance(context, 'unwrapped.InnerFooId', [theLong: 200L, theString: 'MyString'])])]) - - def result = writeJson(jsonMapper, foo) - - then: - result == '{"hk_theInt_out":10,"hk_foo_theLong_in_out":200,"hk_foo_theString_in_out":"MyString","value":"TheValue"}' - - when: - def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Foo'))) - - then: - read - read.value == 'TheValue' - read.hashKey.theInt == 10 - read.hashKey.nested.theLong == 200 - read.hashKey.nested.theString == 'MyString' - - cleanup: - context.close() - } - - void "test @JsonUnwrapped - levels 2"() { - given: - def ctx = buildContext("") - - when: - def nestedEntity = new NestedEntity(); - nestedEntity.setValue("test1"); - NestedEntityId hashKey = new NestedEntityId(); - hashKey.setTheInt(100); - hashKey.setTheString("MyString"); - nestedEntity.setHashKey(hashKey); - Address address = new Address(); - address.getCityData().setCity("NY"); - address.getCityData().setZipCode("22000"); - address.setStreet("Blvd 11"); - nestedEntity.setAddress(address); - def nestedJsonStr = writeJson(jsonMapper, nestedEntity) - - then: - nestedJsonStr == '{"hk_theInt":100,"hk_theString":"MyString","value":"test1","addr_street":"Blvd 11","addr_cd_zipCode":"22000","addr_cd_city":"NY","version":1,"dateCreated":"1970-01-01T00:00:00Z","dateUpdated":"1970-01-01T00:00:00Z"}' - - when: - def deserNestedEntity = jsonMapper.readValue(nestedJsonStr, NestedEntity.class) - - then: - deserNestedEntity - deserNestedEntity.hashKey.theInt == nestedEntity.hashKey.theInt - deserNestedEntity.value == nestedEntity.value - deserNestedEntity.audit.dateCreated == nestedEntity.audit.dateCreated - deserNestedEntity.address.cityData.zipCode == nestedEntity.address.cityData.zipCode - deserNestedEntity.address.street == nestedEntity.address.street - - cleanup: - ctx.close() - } - - void "test @JsonUnwrapped - subtyping"() { - given: - def context = buildContext(""" -package unwrapped; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Wrapper { - @JsonUnwrapped - public final SuperClass name; - - Wrapper(@JsonUnwrapped SuperClass name) { - this.name = name; - } -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -abstract class SuperClass { - public final String first; - SuperClass(String first) { - this.first = first; - } -} - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class SubClass extends SuperClass { - public final String last; - SubClass(String first, String last) { - super(first); - this.last = last; - } -} -""") - when: - def name = newInstance(context, 'unwrapped.SubClass', "Fred", "Flinstone") - def wrapper = newInstance(context, 'unwrapped.Wrapper', name) - - def result = writeJson(jsonMapper, wrapper) - - then: - result == '{"first":"Fred","last":"Flinstone"}' - - cleanup: - context.close() - } - - void 'test wrapped subtype with property info'() { - given: - def context = buildContext('test.Base', """ -package test; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Wrapper { - public final String foo; - @JsonUnwrapped - public final Base base; - - Wrapper(String foo, @JsonUnwrapped Base base) { - this.base = base; - this.foo = foo; - } -} - -@Serdeable -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") -@JsonSubTypes( - @JsonSubTypes.Type(value = Sub.class, name = "sub-class") -) -class Base { - private String string; - - public Base(String string) { - this.string = string; - } - - public String getString() { - return string; - } -} - -@Serdeable -class Sub extends Base { - private Integer integer; - - public Sub(String string, Integer integer) { - super(string); - this.integer = integer; - } - - public Integer getInteger() { - return integer; - } -} -""") - when: - def base = newInstance(context, 'test.Sub', "a", 1) - def wrapper = newInstance(context, 'test.Wrapper', "bar", base) - - def result = writeJson(jsonMapper, wrapper) - - then: - result == '{"foo":"bar","type":"sub-class","string":"a","integer":1}' - - when: - result = jsonMapper.readValue(result, argumentOf(context, "test.Wrapper")) - - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 - - when: - result = jsonMapper.readValue('{"string":"a","integer":1,"type":"sub-class","foo":"bar"}', argumentOf(context, "test.Wrapper")) - - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 - - when: - result = jsonMapper.readValue('{"foo":"bar", "type":"some-other-type","string":"a","integer":1}', argumentOf(context, "test.Wrapper")) - - then: - result.getClass().name != 'test.Sub' - - when: - result = jsonMapper.readValue('{"string":"a","integer":1,"foo":"bar","type":"Sub"}', argumentOf(context, "test.Wrapper")) - - then: - result.getClass().name != 'test.Sub' - } - - void 'test wrapped subtype with wrapper info'() { - given: - def context = buildContext('test.Base', """ -package test; - -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.core.annotation.Introspected; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -@Introspected(accessKind = Introspected.AccessKind.FIELD) -class Wrapper { - public final String foo; - @JsonUnwrapped - public final Base base; - - Wrapper(String foo, @JsonUnwrapped Base base) { - this.base = base; - this.foo = foo; - } -} - -@Serdeable -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) -@JsonSubTypes( - @JsonSubTypes.Type(value = Sub.class, name = "subClass") -) -class Base { - private String string; - - public Base(String string) { - this.string = string; - } - - public String getString() { - return string; - } -} - -@Serdeable -class Sub extends Base { - private Integer integer; - - public Sub(String string, Integer integer) { - super(string); - this.integer = integer; - } - - public Integer getInteger() { - return integer; - } -} -""") - when: - def result = jsonMapper.readValue('{"foo":"bar","subClass":{"string":"a","integer":1}}', argumentOf(context, "test.Wrapper")) - - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 - - when: - result = jsonMapper.readValue('{"subClass":{"string":"a","integer":1}, "foo":"bar"}', argumentOf(context, "test.Wrapper")) - - then: - result.foo == 'bar' - result.base.getClass().name == 'test.Sub' - result.base.string == 'a' - result.base.integer == 1 - - when: - def json = writeJson(jsonMapper, result) - - then: - json == '{"foo":"bar","subClass":{"string":"a","integer":1}}' - } -} diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy new file mode 100644 index 000000000..64e0d69af --- /dev/null +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonPropertySpec.groovy @@ -0,0 +1,179 @@ +package io.micronaut.serde.jackson.annotation + +import io.micronaut.core.beans.exceptions.IntrospectionException +import io.micronaut.serde.jackson.JsonPropertySpec + +class SerdeJsonPropertySpec extends JsonPropertySpec { + + void "test required primitive field"() { + given: + def ctx = buildContext('test.Test', """ +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + @JsonProperty(required = true) + private int value; + + @JsonCreator + Test(@JsonProperty("value") int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} +""") + + when: + def bean = jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) + then: "Jackson will deserialize a default value" + def e = thrown(Exception) + e.message.contains("Unable to deserialize type [test.Test]. Required constructor parameter [int value] at index [0] is not present or is null in the supplied data") + + cleanup: + ctx.close() + } + + + void "test @JsonProperty on field"() { + // Jackson is using 'defaultValue' only for documentation + given: + def context = buildContext('test.Test', """ +package test; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + @JsonProperty(value = "other", defaultValue = "default") + private String value; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private boolean ignored; + public void setValue(String value) { + this.value = value; + } + public String getValue() { + return value; + } + + public void setIgnored(boolean b) { + this.ignored = b; + } + + public boolean isIgnored() { + return ignored; + } +} +""", [value: 'test']) + when: + def result = writeJson(jsonMapper, beanUnderTest) + + then: + result == '{"other":"test","ignored":false}' + + when: + def bean = jsonMapper.readValue(result, argumentOf(context, 'test.Test')) + then: + bean.ignored == false + bean.value == 'test' + + when: + bean = jsonMapper.readValue("{}", argumentOf(context, 'test.Test')) + then: + bean.ignored == false + bean.value == 'default' + + cleanup: + context.close() + + } + + void "test @JsonProperty records"() { + // Jackson is using 'defaultValue' only for documentation + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Test( + @JsonProperty(value = "other", defaultValue = "default") + String value, + @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "false") + boolean ignored +) {} +""") + when: + def bean = newInstance(context, 'test.Test', "test", false) + def result = writeJson(jsonMapper, bean) + + then: + result == '{"other":"test","ignored":false}' + + when: + bean = jsonMapper.readValue(result, argumentOf(context, 'test.Test')) + + then: + bean.ignored == false + bean.value == 'test' + + when: + bean = jsonMapper.readValue('{}', argumentOf(context, 'test.Test')) + + then: + bean.ignored == false + bean.value == 'default' + + cleanup: + context.close() + + } + + void "test @JsonProperty records - invalid default value"() { + // Jackson is using 'defaultValue' only for documentation + given: + def context = buildContext(""" +package test; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable(validate = false) +record Test( + @JsonProperty(value = "other", defaultValue = "default") + String value, + @JsonProperty(access = JsonProperty.Access.READ_ONLY, defaultValue = "junk") + int number +) {} +""") + when: + def bean = newInstance(context, 'test.Test', "test", 10) + def result = writeJson(jsonMapper, bean) + + then: + result == '{"other":"test","number":10}' + + when: + jsonMapper.readValue('{}', argumentOf(context, 'test.Test')) + + then: + def e = thrown(IntrospectionException) + e.cause.message.contains("Constructor Argument [int number] of type [test.Test] defines an invalid default value") + + cleanup: + context.close() + } + +} diff --git a/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonUnwrappedSpec.groovy b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonUnwrappedSpec.groovy new file mode 100644 index 000000000..d22cfd184 --- /dev/null +++ b/serde-jackson/src/test/groovy/io/micronaut/serde/jackson/annotation/SerdeJsonUnwrappedSpec.groovy @@ -0,0 +1,485 @@ +package io.micronaut.serde.jackson.annotation + +import io.micronaut.core.type.Argument +import io.micronaut.serde.jackson.JsonUnwrappedSpec + +class SerdeJsonUnwrappedSpec extends JsonUnwrappedSpec { + + // This cases are not supported by Databind + + // TODO: Correct properties order in the Databind output (unwrappeded should keep it's position) + void "test @JsonUnwrapped - levels 2 - Serde"() { + given: + def ctx = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.serde.annotation.Serdeable; +import java.sql.Timestamp; +import java.util.Date; +import io.micronaut.core.annotation.NonNull; + +@Serdeable +class NestedEntity { + + @JsonUnwrapped(prefix = "hk_") + private NestedEntityId hashKey; + + private String value; + + @JsonUnwrapped(prefix = "addr_") + private Address address; + + @JsonUnwrapped + private Audit audit = new Audit(); + + public NestedEntityId getHashKey() { + return hashKey; + } + + public void setHashKey(NestedEntityId hashKey) { + this.hashKey = hashKey; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Audit getAudit() { + return audit; + } + + public void setAudit(Audit audit) { + this.audit = audit; + } + +} + +@Serdeable +class NestedEntityId { + + private Integer theInt; + + private String theString; + + public Integer getTheInt() { + return theInt; + } + + public void setTheInt(Integer theInt) { + this.theInt = theInt; + } + + public String getTheString() { + return theString; + } + + public void setTheString(String theString) { + this.theString = theString; + } +} + +@Serdeable +class Address { + + @JsonUnwrapped(prefix = "cd_") + private CityData cityData = new CityData(); + + private String street; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public CityData getCityData() { + return cityData; + } + + public void setCityData(CityData cityData) { + this.cityData = cityData; + } +} + +@Serdeable +class Audit { + + static final Timestamp MIN_TIMESTAMP = new Timestamp(new Date(0).getTime()); + + private Long version = 1L; + + // Init manually because cannot be nullable and not getting populated by the event + private Timestamp dateCreated = MIN_TIMESTAMP; + + private Timestamp dateUpdated = MIN_TIMESTAMP; + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Timestamp getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Timestamp dateCreated) { + this.dateCreated = dateCreated; + } + + public Timestamp getDateUpdated() { + return dateUpdated; + } + + public void setDateUpdated(Timestamp dateUpdated) { + this.dateUpdated = dateUpdated; + } +} + +@Serdeable +class CityData { + + @NonNull + private String zipCode; + + private String city; + + public String getZipCode() { + return zipCode; + } + + public void setZipCode(String zipCode) { + this.zipCode = zipCode; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } +} + + +""") + + when: + def nestedJsonStr = '{"hk_theInt":100,"hk_theString":"MyString","value":"test1","addr_street":"Blvd 11","addr_cd_zipCode":"22000","addr_cd_city":"NY","version":1,"dateCreated":"1970-01-01T00:00:00Z","dateUpdated":"1970-01-01T00:00:00Z"}' + def deserNestedEntity = jsonMapper.readValue(nestedJsonStr, Argument.of(ctx.classLoader.loadClass('unwrapped.NestedEntity'))) + + then: + deserNestedEntity + deserNestedEntity.hashKey.theInt == 100 + deserNestedEntity.hashKey.theString == "MyString" + deserNestedEntity.value == "test1" + deserNestedEntity.address.cityData.zipCode == "22000" + deserNestedEntity.address.cityData.city == "NY" + deserNestedEntity.address.street == "Blvd 11" + deserNestedEntity.audit.version == 1 + deserNestedEntity.audit.dateCreated + deserNestedEntity.audit.dateUpdated == deserNestedEntity.audit.dateCreated + + when: + def result = jsonMapper.writeValueAsString(deserNestedEntity) + + then: + result == nestedJsonStr + + cleanup: + ctx.close() + } + + void "test @JsonUnwrapped records"() { + given: + def context = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Parent( + int age, + @JsonUnwrapped + Name name) { +} + +@Serdeable +record Name( + String first, String last +) {} +""") + + when: + def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") + def parent = newInstance(context, 'unwrapped.Parent', 10, name) + + def result = writeJson(jsonMapper, parent) + + then: + result == '{"age":10,"first":"Fred","last":"Flinstone"}' + + when: + def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) + + then: + read.age == 10 + read.name.first == 'Fred' + read.name.last == "Flinstone" + + cleanup: + context.close() + } + + + void "test @JsonUnwrapped - parent constructor args"() { + given: + def context = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Parent { + public final int age; + @JsonUnwrapped + public final Name name; + + Parent(int age, @JsonUnwrapped Name name) { + this.age = age; + this.name = name; + } +} + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Name { + public final String first, last; + Name(String first, String last) { + this.first = first; + this.last = last; + } +} +""") + + when: + def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") + def parent = newInstance(context, 'unwrapped.Parent', 10, name) + + def result = writeJson(jsonMapper, parent) + + then: + result == '{"age":10,"first":"Fred","last":"Flinstone"}' + + when: + def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) + + then: + read.age == 10 + read.name.first == 'Fred' + read.name.last == "Flinstone" + + cleanup: + context.close() + } + + + void 'test wrapped subtype with property info'() { + given: + def context = buildContext('test.Base', """ +package test; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Wrapper { + public final String foo; + @JsonUnwrapped + public final Base base; + + Wrapper(String foo, @JsonUnwrapped Base base) { + this.base = base; + this.foo = foo; + } +} + +@Serdeable +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes( + @JsonSubTypes.Type(value = Sub.class, name = "sub-class") +) +class Base { + private String string; + + public Base(String string) { + this.string = string; + } + + public String getString() { + return string; + } +} + +@Serdeable +class Sub extends Base { + private Integer integer; + + public Sub(String string, Integer integer) { + super(string); + this.integer = integer; + } + + public Integer getInteger() { + return integer; + } +} +""") + when: + def base = newInstance(context, 'test.Sub', "a", 1) + def wrapper = newInstance(context, 'test.Wrapper', "bar", base) + + def result = writeJson(jsonMapper, wrapper) + + then: + result == '{"foo":"bar","type":"sub-class","string":"a","integer":1}' + + when: + result = jsonMapper.readValue(result, argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"string":"a","integer":1,"type":"sub-class","foo":"bar"}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"foo":"bar", "type":"some-other-type","string":"a","integer":1}', argumentOf(context, "test.Wrapper")) + + then: + result.getClass().name != 'test.Sub' + + when: + result = jsonMapper.readValue('{"string":"a","integer":1,"foo":"bar","type":"Sub"}', argumentOf(context, "test.Wrapper")) + + then: + result.getClass().name != 'test.Sub' + } + + void 'test wrapped subtype with wrapper info'() { + given: + def context = buildContext('test.Base', """ +package test; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Wrapper { + public final String foo; + @JsonUnwrapped + public final Base base; + + Wrapper(String foo, @JsonUnwrapped Base base) { + this.base = base; + this.foo = foo; + } +} + +@Serdeable +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonSubTypes( + @JsonSubTypes.Type(value = Sub.class, name = "subClass") +) +class Base { + private String string; + + public Base(String string) { + this.string = string; + } + + public String getString() { + return string; + } +} + +@Serdeable +class Sub extends Base { + private Integer integer; + + public Sub(String string, Integer integer) { + super(string); + this.integer = integer; + } + + public Integer getInteger() { + return integer; + } +} +""") + when: + def result = jsonMapper.readValue('{"foo":"bar","subClass":{"string":"a","integer":1}}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"subClass":{"string":"a","integer":1}, "foo":"bar"}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + def json = writeJson(jsonMapper, result) + + then: + json == '{"foo":"bar","subClass":{"string":"a","integer":1}}' + } + +} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java deleted file mode 100644 index f777fa4ba..000000000 --- a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Address.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class Address { - - @JsonUnwrapped(prefix = "cd_") - private CityData cityData = new CityData(); - - private String street; - - public String getStreet() { - return street; - } - - public void setStreet(String street) { - this.street = street; - } - - public CityData getCityData() { - return cityData; - } - - public void setCityData(CityData cityData) { - this.cityData = cityData; - } -} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java deleted file mode 100644 index e9831d8ac..000000000 --- a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/Audit.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.serde.annotation.Serdeable; - -import java.sql.Timestamp; -import java.util.Date; - -@Serdeable -public class Audit { - - static final Timestamp MIN_TIMESTAMP = new Timestamp(new Date(0).getTime()); - - private Long version = 1L; - - // Init manually because cannot be nullable and not getting populated by the event - private Timestamp dateCreated = MIN_TIMESTAMP; - - private Timestamp dateUpdated = MIN_TIMESTAMP; - - public Long getVersion() { - return version; - } - - public void setVersion(Long version) { - this.version = version; - } - - public Timestamp getDateCreated() { - return dateCreated; - } - - public void setDateCreated(Timestamp dateCreated) { - this.dateCreated = dateCreated; - } - - public Timestamp getDateUpdated() { - return dateUpdated; - } - - public void setDateUpdated(Timestamp dateUpdated) { - this.dateUpdated = dateUpdated; - } -} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java deleted file mode 100644 index bc710a552..000000000 --- a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/CityData.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.core.annotation.NonNull; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class CityData { - - @NonNull - private String zipCode; - - private String city; - - public String getZipCode() { - return zipCode; - } - - public void setZipCode(String zipCode) { - this.zipCode = zipCode; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } -} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java deleted file mode 100644 index b02d286d8..000000000 --- a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntity.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import com.fasterxml.jackson.annotation.JsonUnwrapped; -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class NestedEntity { - - @JsonUnwrapped(prefix = "hk_") - private NestedEntityId hashKey; - - private String value; - - @JsonUnwrapped(prefix = "addr_") - private Address address; - - @JsonUnwrapped - private Audit audit = new Audit(); - - public NestedEntityId getHashKey() { - return hashKey; - } - - public void setHashKey(NestedEntityId hashKey) { - this.hashKey = hashKey; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public Address getAddress() { - return address; - } - - public void setAddress(Address address) { - this.address = address; - } - - public Audit getAudit() { - return audit; - } - - public void setAudit(Audit audit) { - this.audit = audit; - } - -} diff --git a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java b/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java deleted file mode 100644 index 9ebf909b4..000000000 --- a/serde-jackson/src/test/java/io/micronaut/serde/jackson/nested/NestedEntityId.java +++ /dev/null @@ -1,27 +0,0 @@ -package io.micronaut.serde.jackson.nested; - -import io.micronaut.serde.annotation.Serdeable; - -@Serdeable -public class NestedEntityId { - - private Integer theInt; - - private String theString; - - public Integer getTheInt() { - return theInt; - } - - public void setTheInt(Integer theInt) { - this.theInt = theInt; - } - - public String getTheString() { - return theString; - } - - public void setTheString(String theString) { - this.theString = theString; - } -} diff --git a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonPropertyMapper.java b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonPropertyMapper.java index 928a0475e..58af95046 100644 --- a/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonPropertyMapper.java +++ b/serde-processor/src/main/java/io/micronaut/serde/processor/jackson/JsonPropertyMapper.java @@ -15,10 +15,6 @@ */ package io.micronaut.serde.processor.jackson; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; - import io.micronaut.core.annotation.AnnotationValue; import io.micronaut.core.annotation.AnnotationValueBuilder; import io.micronaut.core.bind.annotation.Bindable; @@ -26,6 +22,10 @@ import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.serde.config.annotation.SerdeConfig; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + /** * Maps the {@code com.fasterxml.jackson.annotation.JsonProperty} annotation to {@link SerdeConfig}. */ @@ -42,24 +42,21 @@ public List> map(AnnotationValue annotation, Visi values.add(AnnotationValue.builder(SerdeConfig.META_ANNOTATION_PROPERTY).build()); } - annotation.stringValue("defaultValue") - .ifPresent(s -> - values.add(AnnotationValue.builder(Bindable.class) - .member("defaultValue", s).build()) + annotation.stringValue("defaultValue").ifPresent(s -> + values.add(AnnotationValue.builder(Bindable.class).member("defaultValue", s).build()) ); - final String access = annotation.stringValue("access").orElse(null); - if (access != null) { + annotation.stringValue("access").ifPresent(access -> { switch (access) { - case "READ_ONLY": - builder.member("readOnly", true); - break; - case "WRITE_ONLY": - builder.member("writeOnly", true); - break; - default: - // no-op + case "READ_ONLY": + builder.member("readOnly", true); + break; + case "WRITE_ONLY": + builder.member("writeOnly", true); + break; + default: + // no-op } - } + }); if (annotation.booleanValue("required").orElse(false)) { builder.member(SerdeConfig.REQUIRED, true); } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java index 312eaaa8b..e74aa8c59 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/deserializers/DeserBean.java @@ -594,7 +594,7 @@ private static Deserializer findDeserializer(Deserializer.DecoderContext private boolean isIgnored(BeanProperty bp) { final AnnotationMetadata annotationMetadata = bp.getAnnotationMetadata(); - return annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.WRITE_ONLY).orElse(false) + return annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.READ_ONLY).orElse(false) || annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false) || annotationMetadata.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED_DESERIALIZATION).orElse(false); } diff --git a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java index c919b4251..e60ca28ee 100644 --- a/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java +++ b/serde-support/src/main/java/io/micronaut/serde/support/serializers/SerBean.java @@ -416,7 +416,7 @@ private boolean filterProperty(BeanProperty property) { return !property.isWriteOnly() && !property.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED).orElse(false) && !property.booleanValue(SerdeConfig.class, SerdeConfig.IGNORED_SERIALIZATION).orElse(false) - && !property.booleanValue(SerdeConfig.class, SerdeConfig.READ_ONLY).orElse(false); + && !property.booleanValue(SerdeConfig.class, SerdeConfig.WRITE_ONLY).orElse(false); } static final class PropSerProperty extends SerProperty { diff --git a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy index a60f50c41..13e83fafd 100644 --- a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy +++ b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonPropertySpec.groovy @@ -1,6 +1,107 @@ package io.micronaut.serde.tck.jackson.databind +import io.micronaut.context.ApplicationContextBuilder import io.micronaut.serde.jackson.JsonPropertySpec +import spock.lang.PendingFeature class DatabindJsonPropertySpec extends JsonPropertySpec { + + @Override + protected void configureContext(ApplicationContextBuilder contextBuilder) { + super.configureContext(contextBuilder.properties( + Map.of("jackson.deserialization.failOnUnknownProperties", "true") + )) + } + + void "test required primitive field"() { + + given: + def ctx = buildContext('test.Test', """ +package test; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + @JsonProperty(required = true) + private int value; + + @JsonCreator + Test(@JsonProperty("value") int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} +""") + + when: + def bean = jsonMapper.readValue('{}', argumentOf(ctx, 'test.Test')) + then: + bean.value == 0 + + cleanup: + ctx.close() + } + + @PendingFeature(reason = "Jackson is using 'defaultValue' only for documentation") + void "test @JsonProperty on field"() { + given: + def context = buildContext('test.Test', """ +package test; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +class Test { + @JsonProperty(value = "other", defaultValue = "default") + private String value; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private boolean ignored; + + public void setValue(String value) { + this.value = value; + } + public String getValue() { + return value; + } + + public void setIgnored(boolean b) { + this.ignored = b; + } + + public boolean isIgnored() { + return ignored; + } +} +""", [value: 'test']) + when: + def result = writeJson(jsonMapper, beanUnderTest) + + then: + result == '{"ignored":false,"other":"test"}' + + when: + def bean = jsonMapper.readValue(result, argumentOf(context, 'test.Test')) + then: + bean.ignored == false + bean.value == 'test' + + when: + bean = jsonMapper.readValue("{}", argumentOf(context, 'test.Test')) + then: + bean.ignored == false + bean.value == 'default' + + cleanup: + context.close() + + } + } diff --git a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonUnwrappedSpec.groovy b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonUnwrappedSpec.groovy index 7d32c091e..855106f78 100644 --- a/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonUnwrappedSpec.groovy +++ b/test-suite-tck-jackson-databind/src/test/groovy/io/micronaut/serde/tck/jackson/databind/DatabindJsonUnwrappedSpec.groovy @@ -1,8 +1,288 @@ package io.micronaut.serde.tck.jackson.databind +import io.micronaut.core.type.Argument import io.micronaut.serde.jackson.JsonUnwrappedSpec -import spock.lang.Ignore +import spock.lang.PendingFeature -@Ignore // TODO class DatabindJsonUnwrappedSpec extends JsonUnwrappedSpec { + + // This cases are not supported by Jackson + + @PendingFeature + void "test @JsonUnwrapped records"() { + given: + def context = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +record Parent( + int age, + @JsonUnwrapped + Name name) { +} + +@Serdeable +record Name( + String first, String last +) {} +""") + + when: + def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") + def parent = newInstance(context, 'unwrapped.Parent', 10, name) + + def result = writeJson(jsonMapper, parent) + + then: + result == '{"age":10,"first":"Fred","last":"Flinstone"}' + + when: + def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) + + then: + read.age == 10 + read.name.first == 'Fred' + read.name.last == "Flinstone" + + cleanup: + context.close() + } + + @PendingFeature + void "test @JsonUnwrapped - parent constructor args"() { + given: + def context = buildContext(""" +package unwrapped; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Parent { + public final int age; + @JsonUnwrapped + public final Name name; + + Parent(int age, @JsonUnwrapped Name name) { + this.age = age; + this.name = name; + } +} + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Name { + public final String first, last; + Name(String first, String last) { + this.first = first; + this.last = last; + } +} +""") + + when: + def name = newInstance(context, 'unwrapped.Name', "Fred", "Flinstone") + def parent = newInstance(context, 'unwrapped.Parent', 10, name) + + def result = writeJson(jsonMapper, parent) + + then: + result == '{"age":10,"first":"Fred","last":"Flinstone"}' + + when: + def read = jsonMapper.readValue(result, Argument.of(context.classLoader.loadClass('unwrapped.Parent'))) + + then: + read.age == 10 + read.name.first == 'Fred' + read.name.last == "Flinstone" + + cleanup: + context.close() + } + + @PendingFeature + void 'test wrapped subtype with property info'() { + given: + def context = buildContext('test.Base', """ +package test; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Wrapper { + public final String foo; + @JsonUnwrapped + public final Base base; + + Wrapper(String foo, @JsonUnwrapped Base base) { + this.base = base; + this.foo = foo; + } +} + +@Serdeable +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes( + @JsonSubTypes.Type(value = Sub.class, name = "sub-class") +) +class Base { + private String string; + + public Base(String string) { + this.string = string; + } + + public String getString() { + return string; + } +} + +@Serdeable +class Sub extends Base { + private Integer integer; + + public Sub(String string, Integer integer) { + super(string); + this.integer = integer; + } + + public Integer getInteger() { + return integer; + } +} +""") + when: + def base = newInstance(context, 'test.Sub', "a", 1) + def wrapper = newInstance(context, 'test.Wrapper', "bar", base) + + def result = writeJson(jsonMapper, wrapper) + + then: + result == '{"foo":"bar","type":"sub-class","string":"a","integer":1}' + + when: + result = jsonMapper.readValue(result, argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"string":"a","integer":1,"type":"sub-class","foo":"bar"}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"foo":"bar", "type":"some-other-type","string":"a","integer":1}', argumentOf(context, "test.Wrapper")) + + then: + result.getClass().name != 'test.Sub' + + when: + result = jsonMapper.readValue('{"string":"a","integer":1,"foo":"bar","type":"Sub"}', argumentOf(context, "test.Wrapper")) + + then: + result.getClass().name != 'test.Sub' + } + + @PendingFeature + void 'test wrapped subtype with wrapper info'() { + given: + def context = buildContext('test.Base', """ +package test; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import io.micronaut.core.annotation.Introspected; +import io.micronaut.serde.annotation.Serdeable; + +@Serdeable +@Introspected(accessKind = Introspected.AccessKind.FIELD) +class Wrapper { + public final String foo; + @JsonUnwrapped + public final Base base; + + Wrapper(String foo, @JsonUnwrapped Base base) { + this.base = base; + this.foo = foo; + } +} + +@Serdeable +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonSubTypes( + @JsonSubTypes.Type(value = Sub.class, name = "subClass") +) +class Base { + private String string; + + public Base(String string) { + this.string = string; + } + + public String getString() { + return string; + } +} + +@Serdeable +class Sub extends Base { + private Integer integer; + + public Sub(String string, Integer integer) { + super(string); + this.integer = integer; + } + + public Integer getInteger() { + return integer; + } +} +""") + when: + def result = jsonMapper.readValue('{"foo":"bar","subClass":{"string":"a","integer":1}}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + result = jsonMapper.readValue('{"subClass":{"string":"a","integer":1}, "foo":"bar"}', argumentOf(context, "test.Wrapper")) + + then: + result.foo == 'bar' + result.base.getClass().name == 'test.Sub' + result.base.string == 'a' + result.base.integer == 1 + + when: + def json = writeJson(jsonMapper, result) + + then: + json == '{"foo":"bar","subClass":{"string":"a","integer":1}}' + } + }