//check how the object is constructed
private DeferredParameter loadComplexObject(Object param, Map<Object, DeferredParameter> existing,
            Class<?> expectedType, boolean relaxedValidation) {
        //a list of steps that are performed on the object after it has been created
        //we need to create all these first, to ensure the required objects have already
        //been deserialized
        List<SerialzationStep> setupSteps = new ArrayList<>();
        List<SerialzationStep> ctorSetupSteps = new ArrayList<>();
        boolean relaxedOk = false;
        if (param instanceof Collection) {
            //if this is a collection we want to serialize every element
            for (Object i : (Collection) param) {
                DeferredParameter val = loadObjectInstance(i, existing, i.getClass(), relaxedValidation);
                setupSteps.add(new SerialzationStep() {
                    @Override
                    public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) {
                        //each step can happen in a new method, so it is safe to do this
                        method.invokeInterfaceMethod(COLLECTION_ADD, context.loadDeferred(out), context.loadDeferred(val));
                    }
                    @Override
                    public void prepare(MethodContext context) {
                        //handle the value serialization
                        val.prepare(context);
                    }
                });
            }
            relaxedOk = true;
        }
        if (param instanceof Map) {
            //map works the same as collection
            for (Map.Entry<?, ?> i : ((Map<?, ?>) param).entrySet()) {
                DeferredParameter key = loadObjectInstance(i.getKey(), existing, i.getKey().getClass(), relaxedValidation);
                DeferredParameter val = i.getValue() != null
                        ? loadObjectInstance(i.getValue(), existing, i.getValue().getClass(), relaxedValidation)
                        : loadObjectInstance(null, existing, Object.class, relaxedValidation);
                setupSteps.add(new SerialzationStep() {
                    @Override
                    public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) {
                        method.invokeInterfaceMethod(MAP_PUT, context.loadDeferred(out), context.loadDeferred(key),
                                context.loadDeferred(val));
                    }
                    @Override
                    public void prepare(MethodContext context) {
                        key.prepare(context);
                        val.prepare(context);
                    }
                });
            }
            relaxedOk = true;
        }
        //check how the object is constructed
        NonDefaultConstructorHolder nonDefaultConstructorHolder = null;
        DeferredParameter[] nonDefaultConstructorHandles = null;
        //used to resolve the parameter position for @RecordableConstructor
        Map<String, Integer> constructorParamNameMap = new HashMap<>();
        if (nonDefaultConstructors.containsKey(param.getClass())) {
            nonDefaultConstructorHolder = nonDefaultConstructors.get(param.getClass());
            List<Object> params = nonDefaultConstructorHolder.paramGenerator.apply(param);
            if (params.size() != nonDefaultConstructorHolder.constructor.getParameterCount()) {
                throw new RuntimeException("Unable to serialize " + param
                        + " as the wrong number of parameters were generated for "
                        + nonDefaultConstructorHolder.constructor);
            }
            int count = 0;
            nonDefaultConstructorHandles = new DeferredParameter[params.size()];
            for (int i = 0; i < params.size(); i++) {
                Object obj = params.get(i);
                nonDefaultConstructorHandles[i] = loadObjectInstance(obj, existing,
                        nonDefaultConstructorHolder.constructor.getParameterTypes()[count++], relaxedValidation);
            }
        } else {
            for (Constructor<?> ctor : param.getClass().getConstructors()) {
                if (ctor.isAnnotationPresent(RecordableConstructor.class)) {
                    nonDefaultConstructorHolder = new NonDefaultConstructorHolder(ctor, null);
                    nonDefaultConstructorHandles = new DeferredParameter[ctor.getParameterCount()];
                    for (int i = 0; i < ctor.getParameterCount(); ++i) {
                        String name = ctor.getParameters()[i].getName();
                        constructorParamNameMap.put(name, i);
                    }
                    break;
                }
            }
        }
        Set<String> handledProperties = new HashSet<>();
        Property[] desc = PropertyUtils.getPropertyDescriptors(param);
        for (Property i : desc) {
            // check if the getter is ignored
            if ((i.getReadMethod() != null) && (i.getReadMethod().getAnnotation(IgnoreProperty.class) != null)) {
                continue;
            }
            // check if the matching field is ignored
            try {
                if (param.getClass().getDeclaredField(i.getName()).getAnnotation(IgnoreProperty.class) != null) {
                    continue;
                }
            } catch (NoSuchFieldException ignored) {
            }
            Integer ctorParamIndex = constructorParamNameMap.remove(i.name);
            if (i.getReadMethod() != null && i.getWriteMethod() == null && ctorParamIndex == null) {
                try {
                    //read only prop, we may still be able to do stuff with it if it is a collection
                    if (Collection.class.isAssignableFrom(i.getPropertyType())) {
                        //special case, a collection with only a read method
                        //we assume we can just add to the connection
                        handledProperties.add(i.getName());
                        Collection propertyValue = (Collection) i.read(param);
                        if (propertyValue != null && !propertyValue.isEmpty()) {
                            List<DeferredParameter> params = new ArrayList<>();
                            for (Object c : propertyValue) {
                                DeferredParameter toAdd = loadObjectInstance(c, existing, Object.class, relaxedValidation);
                                params.add(toAdd);
                            }
                            setupSteps.add(new SerialzationStep() {
                                @Override
                                public void handle(MethodContext context, MethodCreator method,
                                        DeferredArrayStoreParameter out) {
                                    //get the collection
                                    ResultHandle prop = method.invokeVirtualMethod(
                                            MethodDescriptor.ofMethod(i.getReadMethod()),
                                            context.loadDeferred(out));
                                    for (DeferredParameter i : params) {
                                        //add the parameter
                                        //TODO: this is not guareded against large collections, probably not an issue in practice
                                        method.invokeInterfaceMethod(COLLECTION_ADD, prop, context.loadDeferred(i));
                                    }
                                }
                                @Override
                                public void prepare(MethodContext context) {
                                    for (DeferredParameter i : params) {
                                        i.prepare(context);
                                    }
                                }
                            });
                        }
                    } else if (Map.class.isAssignableFrom(i.getPropertyType())) {
                        //special case, a map with only a read method
                        //we assume we can just add to the map
                        //similar to how collection works above
                        handledProperties.add(i.getName());
                        Map<Object, Object> propertyValue = (Map<Object, Object>) i.read(param);
                        if (propertyValue != null && !propertyValue.isEmpty()) {
                            Map<DeferredParameter, DeferredParameter> def = new LinkedHashMap<>();
                            for (Map.Entry<Object, Object> entry : propertyValue.entrySet()) {
                                DeferredParameter key = loadObjectInstance(entry.getKey(), existing,
                                        Object.class, relaxedValidation);
                                DeferredParameter val = loadObjectInstance(entry.getValue(), existing, Object.class,
                                        relaxedValidation);
                                def.put(key, val);
                            }
                            setupSteps.add(new SerialzationStep() {
                                @Override
                                public void handle(MethodContext context, MethodCreator method,
                                        DeferredArrayStoreParameter out) {
                                    ResultHandle prop = method.invokeVirtualMethod(
                                            MethodDescriptor.ofMethod(i.getReadMethod()),
                                            context.loadDeferred(out));
                                    for (Map.Entry<DeferredParameter, DeferredParameter> e : def.entrySet()) {
                                        method.invokeInterfaceMethod(MAP_PUT, prop, context.loadDeferred(e.getKey()),
                                                context.loadDeferred(e.getValue()));
                                    }
                                }
                                @Override
                                public void prepare(MethodContext context) {
                                    for (Map.Entry<DeferredParameter, DeferredParameter> e : def.entrySet()) {
                                        e.getKey().prepare(context);
                                        e.getValue().prepare(context);
                                    }
                                }
                            });
                        }
                    } else if (!relaxedValidation && !i.getName().equals("class") && !relaxedOk
                            && nonDefaultConstructorHolder == null) {
                        //check if there is actually a field with the name
                        try {
                            i.getReadMethod().getDeclaringClass().getDeclaredField(i.getName());
                            throw new RuntimeException("Cannot serialise field '" + i.getName() + "' on object '" + param
                                    + "' as the property is read only");
                        } catch (NoSuchFieldException e) {
                            //if there is no underlying field then we ignore the property
                        }
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else if (i.getReadMethod() != null && (i.getWriteMethod() != null || ctorParamIndex != null)) {
                //normal javabean property
                try {
                    handledProperties.add(i.getName());
                    Object propertyValue = i.read(param);
                    if (propertyValue == null && ctorParamIndex == null) {
                        //we just assume properties are null by default
                        //TODO: is this a valid assumption? Should we check this by creating an instance?
                        continue;
                    }
                    Class propertyType = i.getPropertyType();
                    if (ctorParamIndex == null
                            && i.getReadMethod().getReturnType() != i.getWriteMethod().getParameterTypes()[0]) {
                        if (relaxedValidation) {
                            //this is a weird situation where the reader and writer are different types
                            //we iterate and try and find a valid setter method for the type we have
                            //OpenAPI does some weird stuff like this
                            for (Method m : param.getClass().getMethods()) {
                                if (m.getName().equals(i.getWriteMethod().getName())) {
                                    if (m.getParameterTypes().length > 0
                                            && m.getParameterTypes()[0].isAssignableFrom(param.getClass())) {
                                        propertyType = m.getParameterTypes()[0];
                                        break;
                                    }
                                }
                            }
                        } else {
                            throw new RuntimeException("Cannot serialise field " + i.getName() + " on object " + param
                                    + " as setter and getters were different types");
                        }
                    }
                    DeferredParameter val = loadObjectInstance(propertyValue, existing,
                            i.getPropertyType(), relaxedValidation);
                    if (ctorParamIndex != null) {
                        nonDefaultConstructorHandles[ctorParamIndex] = val;
                        ctorSetupSteps.add(new SerialzationStep() {
                            @Override
                            public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) {
                            }
                            @Override
                            public void prepare(MethodContext context) {
                                val.prepare(context);
                            }
                        });
                    } else {
                        Class finalPropertyType = propertyType;
                        setupSteps.add(new SerialzationStep() {
                            @Override
                            public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) {
                                method.invokeVirtualMethod(
                                        ofMethod(param.getClass(), i.getWriteMethod().getName(),
                                                i.getWriteMethod().getReturnType(),
                                                finalPropertyType),
                                        context.loadDeferred(out),
                                        context.loadDeferred(val));
                            }
                            @Override
                            public void prepare(MethodContext context) {
                                val.prepare(context);
                            }
                        });
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
        //now handle accessible fields
        for (Field field : param.getClass().getFields()) {
            // check if the field is ignored
            if (field.getAnnotation(IgnoreProperty.class) != null) {
                continue;
            }
            if (!handledProperties.contains(field.getName())) {
                Integer ctorParamIndex = constructorParamNameMap.remove(field.getName());
                if ((ctorParamIndex != null || !Modifier.isFinal(field.getModifiers())) &&
                        !Modifier.isStatic(field.getModifiers())) {
                    try {
                        DeferredParameter val = loadObjectInstance(field.get(param), existing, field.getType(),
                                relaxedValidation);
                        if (ctorParamIndex != null) {
                            nonDefaultConstructorHandles[ctorParamIndex] = val;
                            ctorSetupSteps.add(new SerialzationStep() {
                                @Override
                                public void handle(MethodContext context, MethodCreator method,
                                        DeferredArrayStoreParameter out) {
                                }
                                @Override
                                public void prepare(MethodContext context) {
                                    val.prepare(context);
                                }
                            });
                        } else {
                            setupSteps.add(new SerialzationStep() {
                                @Override
                                public void handle(MethodContext context, MethodCreator method,
                                        DeferredArrayStoreParameter out) {
                                    method.writeInstanceField(
                                            FieldDescriptor.of(param.getClass(), field.getName(), field.getType()),
                                            context.loadDeferred(out),
                                            context.loadDeferred(val));
                                }
                                @Override
                                public void prepare(MethodContext context) {
                                    val.prepare(context);
                                }
                            });
                        }
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        if (!constructorParamNameMap.isEmpty()) {
            throw new RuntimeException("Could not find parameters for constructor " + nonDefaultConstructorHolder.constructor
                    + " could not read field values " + constructorParamNameMap.keySet());
        }
        NonDefaultConstructorHolder finalNonDefaultConstructorHolder = nonDefaultConstructorHolder;
        DeferredParameter[] finalCtorHandles = nonDefaultConstructorHandles;
        //create a deferred value to represet the object itself. This allows the creation to be split
        //over multiple methods, which is important if this is a large object
        DeferredArrayStoreParameter objectValue = new DeferredArrayStoreParameter() {
            @Override
            ResultHandle createValue(MethodContext context, MethodCreator method, ResultHandle array) {
                ResultHandle out;
                //do the creation
                if (finalNonDefaultConstructorHolder != null) {
                    out = method.newInstance(
                            ofConstructor(finalNonDefaultConstructorHolder.constructor.getDeclaringClass(),
                                    finalNonDefaultConstructorHolder.constructor.getParameterTypes()),
                            Arrays.stream(finalCtorHandles).map(m -> context.loadDeferred(m))
                                    .toArray(ResultHandle[]::new));
                } else {
                    try {
                        param.getClass().getDeclaredConstructor();
                        out = method.newInstance(ofConstructor(param.getClass()));
                    } catch (NoSuchMethodException e) {
                        //fallback for collection types, such as unmodifiableMap
                        if (SortedMap.class.isAssignableFrom(expectedType)) {
                            out = method.newInstance(ofConstructor(TreeMap.class));
                        } else if (Map.class.isAssignableFrom(expectedType)) {
                            out = method.newInstance(ofConstructor(LinkedHashMap.class));
                        } else if (List.class.isAssignableFrom(expectedType)) {
                            out = method.newInstance(ofConstructor(ArrayList.class));
                        } else if (SortedSet.class.isAssignableFrom(expectedType)) {
                            out = method.newInstance(ofConstructor(TreeSet.class));
                        } else if (Set.class.isAssignableFrom(expectedType)) {
                            out = method.newInstance(ofConstructor(LinkedHashSet.class));
                        } else {
                            throw new RuntimeException("Unable to serialize objects of type " + param.getClass()
                                    + " to bytecode as it has no default constructor");
                        }
                    }
                }
                return out;
            }
        };
        //now return the actual deferred parameter that represents the result of construction
        return new DeferredArrayStoreParameter() {
            @Override
            void doPrepare(MethodContext context) {
                //this is where the object construction happens
                //first create the actial object
                for (SerialzationStep i : ctorSetupSteps) {
                    i.prepare(context);
                }
                objectValue.prepare(context);
                for (SerialzationStep i : setupSteps) {
                    //then prepare the steps (i.e. creating the values to be placed into this object)
                    i.prepare(context);
                }
                for (SerialzationStep i : setupSteps) {
                    //now actually run the steps (i.e. actually stick the values into the object)
                    context.writeInstruction(new InstructionGroup() {
                        @Override
                        public void write(MethodContext context, MethodCreator method, ResultHandle array) {
                            i.handle(context, method, objectValue);
                        }
                    });
                }
                super.doPrepare(context);
            }
            @Override
            ResultHandle createValue(MethodContext context, MethodCreator method, ResultHandle array) {
                //just return the already created object
                return context.loadDeferred(objectValue);
            }
        };
    }
relaxedOk = true;
}
//check how the object is constructed
NonDefaultConstructorHolder nonDefaultConstructorHolder = null;
DeferredParameter[] nonDefaultConstructorHandles = null;
}
@Override
public void prepare(MethodContext context) {
key.prepare(context);
val.prepare(context);
}
});
}
relaxedOk = true;
}
//check how the object is constructed
NonDefaultConstructorHolder nonDefaultConstructorHolder = null;
DeferredParameter[] nonDefaultConstructorHandles = null;
//used to resolve the parameter position for @RecordableConstructor
Map<String, Integer> constructorParamNameMap = new HashMap<>();
if (nonDefaultConstructors.containsKey(param.getClass())) {
nonDefaultConstructorHolder = nonDefaultConstructors.get(param.getClass());
List<Object> params = nonDefaultConstructorHolder.paramGenerator.apply(param);
if (params.size() != nonDefaultConstructorHolder.constructor.getParameterCount()) {
throw new RuntimeException("Unable to serialize " + param
+ " as the wrong number of parameters were generated for "
for (Map.Entry<?, ?> i : ((Map<?, ?>) param).entrySet()) {
DeferredParameter key = loadObjectInstance(i.getKey(), existing, i.getKey().getClass(), relaxedValidation);
DeferredParameter val = i.getValue() != null
? loadObjectInstance(i.getValue(), existing, i.getValue().getClass(), relaxedValidation)
: loadObjectInstance(null, existing, Object.class, relaxedValidation);
setupSteps.add(new SerialzationStep() {
@Override
public void handle(MethodContext context, MethodCreator method, DeferredArrayStoreParameter out) {
method.invokeInterfaceMethod(MAP_PUT, context.loadDeferred(out), context.loadDeferred(key),
context.loadDeferred(val));
}
@Override
public void prepare(MethodContext context) {
key.prepare(context);
val.prepare(context);
}
});
}
relaxedOk = true;
}
//check how the object is constructed
NonDefaultConstructorHolder nonDefaultConstructorHolder = null;
DeferredParameter[] nonDefaultConstructorHandles = null;
//used to resolve the parameter position for @RecordableConstructor
Map<String, Integer> constructorParamNameMap = new HashMap<>();
if (nonDefaultConstructors.containsKey(param.getClass())) {
nonDefaultConstructorHolder = nonDefaultConstructors.get(param.getClass());
List<Object> params = nonDefaultConstructorHolder.paramGenerator.apply(param);
if (params.size() != nonDefaultConstructorHolder.constructor.getParameterCount()) {
throw new RuntimeException("Unable to serialize " + param
+ " as the wrong number of parameters were generated for "
+ nonDefaultConstructorHolder.constructor);
}
int count = 0;
nonDefaultConstructorHandles = new DeferredParameter[params.size()];
for (int i = 0; i < params.size(); i++) {
Object obj = params.get(i);
nonDefaultConstructorHandles[i] = loadObjectInstance(obj, existing,
nonDefaultConstructorHolder.constructor.getParameterTypes()[count++], relaxedValidation);
}
} else {