/* * Copyright 2017 MongoDB, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.bson.codecs.pojo; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.pojo.entities.AbstractInterfaceModel; import org.bson.codecs.pojo.entities.CollectionNestedPojoModel; import org.bson.codecs.pojo.entities.ConcreteAndNestedAbstractInterfaceModel; import org.bson.codecs.pojo.entities.ConcreteCollectionsModel; import org.bson.codecs.pojo.entities.ConcreteStandAloneAbstractInterfaceModel; import org.bson.codecs.pojo.entities.ConventionModel; import org.bson.codecs.pojo.entities.ConverterModel; import org.bson.codecs.pojo.entities.FieldReusingClassTypeParameter; import org.bson.codecs.pojo.entities.FieldWithMultipleTypeParamsModel; import org.bson.codecs.pojo.entities.GenericHolderModel; import org.bson.codecs.pojo.entities.GenericTreeModel; import org.bson.codecs.pojo.entities.InterfaceBasedModel; import org.bson.codecs.pojo.entities.MultipleBoundsModel; import org.bson.codecs.pojo.entities.MultipleLevelGenericModel; import org.bson.codecs.pojo.entities.NestedFieldReusingClassTypeParameter; import org.bson.codecs.pojo.entities.NestedGenericHolderFieldWithMultipleTypeParamsModel; import org.bson.codecs.pojo.entities.NestedGenericHolderMapModel; import org.bson.codecs.pojo.entities.NestedGenericHolderModel; import org.bson.codecs.pojo.entities.NestedGenericHolderSimpleGenericsModel; import org.bson.codecs.pojo.entities.NestedGenericTreeModel; import org.bson.codecs.pojo.entities.NestedMultipleLevelGenericModel; import org.bson.codecs.pojo.entities.NestedReusedGenericsModel; import org.bson.codecs.pojo.entities.NestedSelfReferentialGenericHolderModel; import org.bson.codecs.pojo.entities.NestedSelfReferentialGenericModel; import org.bson.codecs.pojo.entities.NoConstructorModel; import org.bson.codecs.pojo.entities.PrimitivesModel; import org.bson.codecs.pojo.entities.ReusedGenericsModel; import org.bson.codecs.pojo.entities.SelfReferentialGenericModel; import org.bson.codecs.pojo.entities.ShapeHolderModel; import org.bson.codecs.pojo.entities.ShapeModelAbstract; import org.bson.codecs.pojo.entities.ShapeModelCircle; import org.bson.codecs.pojo.entities.ShapeModelRectangle; import org.bson.codecs.pojo.entities.SimpleGenericsModel; import org.bson.codecs.pojo.entities.SimpleModel; import org.bson.codecs.pojo.entities.SimpleNestedPojoModel; import org.bson.codecs.pojo.entities.UpperBoundsConcreteModel; import org.bson.types.ObjectId; import org.junit.Test; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; import static org.bson.codecs.pojo.Conventions.NO_CONVENTIONS; public final class PojoCodecTest extends PojoTestCase { @Test public void testRoundTripSimpleModel() { SimpleModel model = getSimpleModel(); roundTrip(getPojoCodecProviderBuilder(SimpleModel.class), model, SIMPLE_MODEL_JSON); } @Test public void testRoundTripPrimitivesModel() { PrimitivesModel model = getPrimitivesModel(); roundTrip(getPojoCodecProviderBuilder(PrimitivesModel.class), model, "{ 'myBoolean': true, 'myByte': 1, 'myCharacter': '1', 'myDouble': 1.0, 'myFloat': 2.0, 'myInteger': 3, " + "'myLong': { '$numberLong': '5' }, 'myShort': 6}"); } @Test public void testRoundTripConcreteCollectionsModel() { ConcreteCollectionsModel model = getConcreteCollectionsModel(); roundTrip(getPojoCodecProviderBuilder(ConcreteCollectionsModel.class), model, "{'collection': [1, 2, 3], 'list': [4, 5, 6], 'linked': [7, 8, 9], 'map': {'A': 1.1, 'B': 2.2, 'C': 3.3}," + "'concurrent': {'D': 4.4, 'E': 5.5, 'F': 6.6}}"); } @Test public void testRoundTripSimpleNestedPojoModel() { SimpleNestedPojoModel model = getSimpleNestedPojoModel(); roundTrip(getPojoCodecProviderBuilder(SimpleNestedPojoModel.class, SimpleModel.class), model, "{'simple': " + SIMPLE_MODEL_JSON + "}"); } @Test public void testRoundTripCollectionNestedPojoModel() { CollectionNestedPojoModel model = getCollectionNestedPojoModel(); roundTrip(getPojoCodecProviderBuilder(CollectionNestedPojoModel.class, SimpleModel.class), model, "{ 'listSimple': [" + SIMPLE_MODEL_JSON + "]," + "'listListSimple': [[" + SIMPLE_MODEL_JSON + "]]," + "'setSimple': [" + SIMPLE_MODEL_JSON + "]," + "'setSetSimple': [[" + SIMPLE_MODEL_JSON + "]]," + "'mapSimple': {'s': " + SIMPLE_MODEL_JSON + "}," + "'mapMapSimple': {'ms': {'s': " + SIMPLE_MODEL_JSON + "}}," + "'mapListSimple': {'ls': [" + SIMPLE_MODEL_JSON + "]}," + "'mapListMapSimple': {'lm': [{'s': " + SIMPLE_MODEL_JSON + "}]}," + "'mapSetSimple': {'s': [" + SIMPLE_MODEL_JSON + "]}," + "'listMapSimple': [{'s': " + SIMPLE_MODEL_JSON + "}]," + "'listMapListSimple': [{'ls': [" + SIMPLE_MODEL_JSON + "]}]," + "'listMapSetSimple': [{'s': [" + SIMPLE_MODEL_JSON + "]}]," + "}"); } @Test public void testShapeModelAbstract() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(ShapeModelAbstract.class, ShapeModelCircle.class, ShapeModelRectangle.class, ShapeHolderModel.class); roundTrip(builder, new ShapeHolderModel(getShapeModelCircle()), "{'shape': {'_t': 'ShapeModelCircle', 'color': 'orange', 'radius': 4.2}}"); roundTrip(builder, new ShapeHolderModel(getShapeModelRectangle()), "{'shape': {'_t': 'ShapeModelRectangle', 'color': 'green', 'width': 22.1, 'height': 105.0} }"); } @Test public void testInheritedDiscriminatorAnnotation() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(ShapeModelCircle.class, ShapeModelRectangle.class); roundTrip(builder, getShapeModelCircle(), "{'_t': 'ShapeModelCircle', 'color': 'orange', 'radius': 4.2}"); roundTrip(builder, getShapeModelRectangle(), "{'_t': 'ShapeModelRectangle', 'color': 'green', 'width': 22.1, 'height': 105.0}"); } @Test public void testUpperBoundsConcreteModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(UpperBoundsConcreteModel.class); roundTrip(builder, new UpperBoundsConcreteModel(1L), "{'myGenericField': {'$numberLong': '1'}}"); } @Test public void testNestedGenericHolderModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedGenericHolderModel.class, GenericHolderModel.class); roundTrip(builder, getNestedGenericHolderModel(), "{'nested': {'myGenericField': 'generic', 'myLongField': {'$numberLong': '1'}}}"); } @Test public void testNestedGenericHolderMapModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedGenericHolderMapModel.class, GenericHolderModel.class, SimpleGenericsModel.class, SimpleModel.class); roundTrip(builder, getNestedGenericHolderMapModel(), "{ 'nested': { 'myGenericField': {'s': " + SIMPLE_MODEL_JSON + "}, 'myLongField': {'$numberLong': '1'}}}"); } @Test public void testNestedReusedGenericsModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedReusedGenericsModel.class, ReusedGenericsModel.class, SimpleModel.class); roundTrip(builder, getNestedReusedGenericsModel(), "{ 'nested':{ 'field1':{ '$numberLong':'1' }, 'field2':[" + SIMPLE_MODEL_JSON + "], " + "'field3':'field3', 'field4':42, 'field5':'field5', 'field6':[" + SIMPLE_MODEL_JSON + ", " + SIMPLE_MODEL_JSON + "], 'field7':{ '$numberLong':'2' }, 'field8':'field8' } }"); } @Test public void testMultipleBoundsModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(MultipleBoundsModel.class); HashMap<String, String> map = new HashMap<String, String>(); map.put("key", "value"); List<Integer> list = asList(1, 2, 3); roundTrip(builder, new MultipleBoundsModel(map, list, 2.2), "{'level1' : 2.2, 'level2': [1, 2, 3], 'level3': {key: 'value'}}"); } @Test public void testNestedGenericHolderFieldWithMultipleTypeParamsModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedGenericHolderFieldWithMultipleTypeParamsModel.class, FieldWithMultipleTypeParamsModel.class, SimpleGenericsModel.class, GenericHolderModel.class).conventions(NO_CONVENTIONS); SimpleGenericsModel<Long, String, Integer> simple = getSimpleGenericsModelAlt(); FieldWithMultipleTypeParamsModel<Integer, Long, String> field = new FieldWithMultipleTypeParamsModel<Integer, Long, String>(simple); GenericHolderModel<FieldWithMultipleTypeParamsModel<Integer, Long, String>> nested = new GenericHolderModel<FieldWithMultipleTypeParamsModel<Integer, Long, String>>(field, 42L); roundTrip(builder, new NestedGenericHolderFieldWithMultipleTypeParamsModel(nested), "{'nested': {'myGenericField': " + "{'simpleGenericsModel': {'myIntegerField': 42, 'myGenericField': {'$numberLong': '101'}, " + " 'myListField': ['B', 'C'], 'myMapField': {'D': 2, 'E': 3, 'F': 4 }}}, 'myLongField': {'$numberLong': '42'}}}"); } @Test public void testNestedGenericTreeModel(){ PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedGenericTreeModel.class, GenericTreeModel.class); roundTrip(builder, new NestedGenericTreeModel(42, getGenericTreeModel()), "{'intField': 42, 'nested': {'field1': 'top', 'field2': 1, " + "'left': {'field1': 'left', 'field2': 2, 'left': {'field1': 'left', 'field2': 3}}, " + "'right': {'field1': 'right', 'field2': 4, 'left': {'field1': 'left', 'field2': 5}}}}"); } @Test public void testNestedMultipleLevelGenericModel() { PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedMultipleLevelGenericModel.class, MultipleLevelGenericModel.class, GenericTreeModel.class); String json = "{'intField': 42, 'nested': {'stringField': 'string', 'nested': {'field1': 'top', 'field2': 1, " + "'left': {'field1': 'left', 'field2': 2, 'left': {'field1': 'left', 'field2': 3}}, " + "'right': {'field1': 'right', 'field2': 4, 'left': {'field1': 'left', 'field2': 5}}}}}"; roundTrip(builder, new NestedMultipleLevelGenericModel(42, new MultipleLevelGenericModel<String>("string", getGenericTreeModel())), json); } @Test public void testGenericsRoundTrip() { // Multiple levels of nesting SimpleModel simpleModel = getSimpleModel(); Map<String, SimpleModel> map = new HashMap<String, SimpleModel>(); map.put("A", simpleModel); Map<String, Map<String, SimpleModel>> mapB = new HashMap<String, Map<String, SimpleModel>>(); mapB.put("A", map); SimpleGenericsModel<Integer, List<SimpleModel>, Map<String, SimpleModel>> simpleGenericsModel = new SimpleGenericsModel<Integer, List<SimpleModel>, Map<String, SimpleModel>>(42, 42, singletonList(singletonList(simpleModel)), mapB); GenericHolderModel<SimpleGenericsModel<Integer, List<SimpleModel>, Map<String, SimpleModel>>> nested = new GenericHolderModel<SimpleGenericsModel<Integer, List<SimpleModel>, Map<String, SimpleModel>>>(simpleGenericsModel, 42L); NestedGenericHolderSimpleGenericsModel model = new NestedGenericHolderSimpleGenericsModel(nested); PojoCodecProvider.Builder builder = getPojoCodecProviderBuilder(NestedGenericHolderSimpleGenericsModel.class, GenericHolderModel.class, SimpleGenericsModel.class, SimpleModel.class); roundTrip(builder, model, "{'nested': {'myGenericField': {'myIntegerField': 42, 'myGenericField': 42," + " 'myListField': [[" + SIMPLE_MODEL_JSON + "]], " + " 'myMapField': {'A': {'A': " + SIMPLE_MODEL_JSON + "}}}," + " 'myLongField': {'$numberLong': '42' }}}"); } @Test public void testNestedFieldReusingClassTypeParameter() { NestedFieldReusingClassTypeParameter model = new NestedFieldReusingClassTypeParameter( new FieldReusingClassTypeParameter<String>(getGenericTreeModelStrings())); roundTrip(getPojoCodecProviderBuilder(NestedFieldReusingClassTypeParameter.class, FieldReusingClassTypeParameter.class, GenericTreeModel.class), model, "{'nested': {'tree': {'field1': 'top', 'field2': '1', " + "'left': {'field1': 'left', 'field2': '2', 'left': {'field1': 'left', 'field2': '3'}}, " + "'right': {'field1': 'right', 'field2': '4', 'left': {'field1': 'left', 'field2': '5'}}}}}"); } @Test public void testSelfReferentialGenerics() { SelfReferentialGenericModel<Boolean, Long> selfRef1 = new SelfReferentialGenericModel<Boolean, Long>(true, 33L, new SelfReferentialGenericModel<Long, Boolean>(44L, false, null)); SelfReferentialGenericModel<Boolean, Double> selfRef2 = new SelfReferentialGenericModel<Boolean, Double>(true, 3.14, new SelfReferentialGenericModel<Double, Boolean>(3.42, true, null)); NestedSelfReferentialGenericModel<Boolean, Long, Double> nested = new NestedSelfReferentialGenericModel<Boolean, Long, Double>(true, 42L, 44.0, selfRef1, selfRef2); NestedSelfReferentialGenericHolderModel model = new NestedSelfReferentialGenericHolderModel(nested); roundTrip(getPojoCodecProviderBuilder(NestedSelfReferentialGenericHolderModel.class, NestedSelfReferentialGenericModel.class, SelfReferentialGenericModel.class), model, "{'nested': { 't': true, 'v': {'$numberLong': '42'}, 'z': 44.0, " + "'selfRef1': {'t': true, 'v': {'$numberLong': '33'}, 'child': {'t': {'$numberLong': '44'}, 'v': false}}, " + "'selfRef2': {'t': true, 'v': 3.14, 'child': {'t': 3.42, 'v': true}}}}"); } @Test public void testInterfaceBasedModel() { InterfaceBasedModel model = new ConcreteAndNestedAbstractInterfaceModel("A", new ConcreteAndNestedAbstractInterfaceModel("B", new ConcreteStandAloneAbstractInterfaceModel("C"))); roundTrip(getPojoCodecProviderBuilder(InterfaceBasedModel.class, AbstractInterfaceModel.class, ConcreteAndNestedAbstractInterfaceModel.class, ConcreteStandAloneAbstractInterfaceModel.class), model, "{'_t': 'ConcreteAndNestedAbstractInterfaceModel', 'name': 'A', " + "'child': {'_t': 'ConcreteAndNestedAbstractInterfaceModel', 'name': 'B', " + " 'child': {'_t': 'ConcreteStandAloneAbstractInterfaceModel', 'name': 'C'}}}}"); } @Test public void testConventionsDefault() { ConventionModel model = getConventionModel(); roundTrip(getPojoCodecProviderBuilder(ConventionModel.class, SimpleModel.class), model, "{'_id': 'id', '_cls': 'AnnotatedConventionModel', 'myFinalField': 10, 'myIntField': 10, " + "'child': {'_id': 'child', 'myFinalField': 10, 'myIntField': 10," + "'model': {'integerField': 42, 'stringField': 'myString'}}}"); } @Test public void testConventionsEmpty() { ConventionModel model = getConventionModel(); ClassModelBuilder<ConventionModel> classModel = ClassModel.builder(ConventionModel.class) .conventions(NO_CONVENTIONS); ClassModelBuilder<SimpleModel> nestedClassModel = ClassModel.builder(SimpleModel.class).conventions(NO_CONVENTIONS); roundTrip(getPojoCodecProviderBuilder(classModel, nestedClassModel), model, "{'myFinalField': 10, 'myIntField': 10, 'customId': 'id'," + "'child': {'myFinalField': 10, 'myIntField': 10, 'customId': 'child'," + " 'simpleModel': {'integerField': 42, 'stringField': 'myString' } } }"); } @Test public void testConventionsCustom() { ConventionModel model = getConventionModel(); List<Convention> conventions = Collections.<Convention>singletonList( new Convention() { @Override public void apply(final ClassModelBuilder<?> classModelBuilder) { for (FieldModelBuilder<?> fieldModelBuilder : classModelBuilder.getFields()) { fieldModelBuilder.discriminatorEnabled(false); fieldModelBuilder.documentFieldName(fieldModelBuilder.getFieldName().replaceAll("([^_A-Z])([A-Z])", "$1_$2") .toLowerCase()); } if (classModelBuilder.getField("customId") != null) { classModelBuilder.idField("customId"); } classModelBuilder.enableDiscriminator(true); classModelBuilder.discriminatorKey("_cls"); classModelBuilder.discriminator(classModelBuilder.getType().getSimpleName() .replaceAll("([^_A-Z])([A-Z])", "$1_$2").toLowerCase()); } }); ClassModelBuilder<ConventionModel> classModel = ClassModel.builder(ConventionModel.class).conventions(conventions); ClassModelBuilder<SimpleModel> nestedClassModel = ClassModel.builder(SimpleModel.class).conventions(conventions); roundTrip(getPojoCodecProviderBuilder(classModel, nestedClassModel), model, "{ '_id': 'id', '_cls': 'convention_model', 'my_final_field': 10, 'my_int_field': 10," + "'child': { '_id': 'child', 'my_final_field': 10, 'my_int_field': 10, " + " 'simple_model': {'integer_field': 42, 'string_field': 'myString' } } }"); } @Test @SuppressWarnings("unchecked") public void testCustomCodec() { ObjectId id = new ObjectId(); ConverterModel model = new ConverterModel(id.toHexString(), "myName"); ClassModelBuilder<ConverterModel> classModel = ClassModel.builder(ConverterModel.class); FieldModelBuilder<String> idFieldModelBuilder = (FieldModelBuilder<String>) classModel.getField("id"); idFieldModelBuilder.codec(new StringToObjectIdCodec()); roundTrip(getPojoCodecProviderBuilder(classModel), model, format("{'_id': {'$oid': '%s'}, 'name': 'myName'}", id.toHexString())); } @Test @SuppressWarnings("unchecked") public void testCustomFieldSerializer() { SimpleModel model = getSimpleModel(); model.setIntegerField(null); ClassModelBuilder<SimpleModel> classModel = ClassModel.builder(SimpleModel.class); ((FieldModelBuilder<Integer>) classModel.getField("integerField")) .fieldSerialization(new FieldSerialization<Integer>() { @Override public boolean shouldSerialize(final Integer value) { return true; } }); roundTrip(getPojoCodecProviderBuilder(classModel), model, "{'integerField': null, 'stringField': 'myString'}"); } @Test @SuppressWarnings("unchecked") public void testCanHandleNullValuesForNestedModels() { SimpleNestedPojoModel model = getSimpleNestedPojoModel(); model.setSimple(null); ClassModelBuilder<SimpleNestedPojoModel> classModel = ClassModel.builder(SimpleNestedPojoModel.class); ((FieldModelBuilder<SimpleModel>) classModel.getField("simple")) .fieldSerialization(new FieldSerialization<SimpleModel>() { @Override public boolean shouldSerialize(final SimpleModel value) { return true; } }); ClassModelBuilder<SimpleModel> classModelSimple = ClassModel.builder(SimpleModel.class); roundTrip(getPojoCodecProviderBuilder(classModel, classModelSimple), model, "{'simple': null}"); } @Test @SuppressWarnings("unchecked") public void testCanHandleNullValuesForCollectionsAndMaps() { ConcreteCollectionsModel model = getConcreteCollectionsModel(); model.setCollection(null); model.setMap(null); ClassModelBuilder<ConcreteCollectionsModel> classModel = ClassModel.builder(ConcreteCollectionsModel.class); ((FieldModelBuilder<Collection<Integer>>) classModel.getField("collection")) .fieldSerialization(new FieldSerialization<Collection<Integer>>() { @Override public boolean shouldSerialize(final Collection<Integer> value) { return true; } }); ((FieldModelBuilder<Map<String, Double>>) classModel.getField("map")) .fieldSerialization(new FieldSerialization<Map<String, Double>>() { @Override public boolean shouldSerialize(final Map<String, Double> value) { return true; } }); roundTrip(getPojoCodecProviderBuilder(classModel), model, "{'collection': null, 'list': [4, 5, 6], 'linked': [7, 8, 9], 'map': null," + "'concurrent': {'D': 4.4, 'E': 5.5, 'F': 6.6}}"); } @Test public void testCanHandleExtraData() { decodesTo(getCodec(SimpleModel.class), "{'integerField': 42, 'stringField': 'myString', 'extraFieldA': 1, 'extraFieldB': 2}", getSimpleModel()); } @Test public void testDataCanHandleMissingData() { SimpleModel model = getSimpleModel(); model.setIntegerField(null); decodesTo(getCodec(SimpleModel.class), "{'_t': 'SimpleModel', 'stringField': 'myString'}", model); } @Test(expected = CodecConfigurationException.class) public void testNoConstructor() { decodingShouldFail(getCodec(NoConstructorModel.class), "{'integerField': 99}"); } @Test(expected = CodecConfigurationException.class) public void testDataUnknownClass() { ClassModel<SimpleModel> classModel = ClassModel.builder(SimpleModel.class).enableDiscriminator(true).build(); decodingShouldFail(getCodec(PojoCodecProvider.builder().register(classModel), SimpleModel.class), "{'_t': 'FakeModel'}"); } @Test(expected = CodecConfigurationException.class) public void testInvalidTypeForField() { decodingShouldFail(getCodec(SimpleModel.class), "{'_t': 'SimpleModel', 'stringField': 123}"); } @Test(expected = CodecConfigurationException.class) public void testInvalidTypeForPrimitiveField() { decodingShouldFail(getCodec(PrimitivesModel.class), "{ '_t': 'PrimitivesModel', 'myBoolean': null}"); } @Test(expected = CodecConfigurationException.class) public void testInvalidTypeForModelField() { decodingShouldFail(getCodec(SimpleNestedPojoModel.class), "{ '_t': 'SimpleNestedPojoModel', 'simple': 123}"); } @Test(expected = CodecConfigurationException.class) public void testInvalidDiscriminatorInNestedModel() { decodingShouldFail(getCodec(SimpleNestedPojoModel.class), "{ '_t': 'SimpleNestedPojoModel'," + "'simple': {'_t': 'FakeModel', 'integerField': 42, 'stringField': 'myString'}}"); } @Test(expected = CodecConfigurationException.class) public void testCannotEncodeUnspecializedClasses() { CodecRegistry registry = fromProviders(getPojoCodecProviderBuilder(GenericTreeModel.class).build()); encode(registry.get(GenericTreeModel.class), getGenericTreeModel()); } @Test(expected = CodecConfigurationException.class) public void testCannotDecodeUnspecializedClasses() { decodingShouldFail(getCodec(GenericTreeModel.class), "{'field1': 'top', 'field2': 1, " + "'left': {'field1': 'left', 'field2': 2, 'left': {'field1': 'left', 'field2': 3}}, " + "'right': {'field1': 'right', 'field2': 4, 'left': {'field1': 'left', 'field2': 5}}}"); } }