14

I have a requirement to get JSON input Pojo instance and I am using Jackson 2 library and below readValue method could deserialise using typeReferencing :

POJO_ClassName p = mapper.readValue(new TypeReference< POJO_ClassName >() {});

But the problem is that as POJO is created and loaded at runtime dynamically, how do I get JSON to POJO instance/object as I do not have fully qualified class (POJO_ClassName) name for above statement?

Note: I use jsonSchema2pojo library to generate POJO classes at runtime.

Here is code snippet, I am using to generate POJO for JSON at runtime and trying

  String classPath="com.EnrichmentService.Thread72"; 
     String classLocation = System.getProperty("user.dir")
                         + "/src/main/java"; JCodeModel codeModel = new JCodeModel();

     final RuleFactory ruleFactory = new RuleFactory(config,
                         new Jackson2Annotator(config), new SchemaStore());

     final SchemaMapper mapperSchema = new SchemaMapper(ruleFactory,
                         new SchemaGenerator());

     mapperSchema.generate(codeModel, "EsRootDoc",classPath, json);

     codeModel.build(new File(classLocation));  // generates pojo classes

     // Till above jsonSchema2Pojo pojo generation all Good !!
      // EsRootDoc instance is needed for further drools drl validations.

     com.EnrichmentService.Thread72.EsRootDoc p = mapper.readValue(new TypeReference<com.EnrichmentService.Thread72.EsRootDoc>() {}); 
// see alternative way as well in my 24Aug17 edit at the end of this question

But as com.EnrichmentService.Thread72.EsRootDoc has yet not been generated compiler would error to class not Found.

Main Points:

1) Same Pojo classes generated at run time iteratively but with different properties as JSON input changes each time.

2) Even tried Object pojo =mapper.readValue(json,Class.forName("com.EnrichmentService.Thread72.EsRootDoc")); as class.forName does not replace an existing class!

Edit 24 Aug17 - Here is my custom class loader :

Note: Indexer is class which load dynamic EsRootDoc/POJO class at run time.

 static class TestClassLoader extends ClassLoader {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                if (name.equals("com.EnrichmentService.Thread72.EsRootDoc")) {
                    try {
                        InputStream is = Indexer.class.getClassLoader().getResourceAsStream("com/EnrichmentService/Thread72/EsRootDoc.class");
                        byte[] buf = new byte[is.available()];
                        int len = is.read(buf);

                        Class<?> c=defineClass(name, buf, 0, len);
                        resolveClass(c);
                        return c;


                    } catch (IOException e) {
                        throw new ClassNotFoundException("", e);
                    }
                }
                return getParent().loadClass(name);
            }
        }

I have tried using above TestClassLoader custom class loader as an alternative way is like this :

Class cls = new      TestClassLoader().loadClass("com.EnrichmentService.Thread72.EsRootDoc");
    Object obj = cls.newInstance();
    cls.getMethod("getCrawlerSource").invoke(obj);
    p=mapper.readValue(json, cls);  // but here i am getting the same deserialization exception as earlier.

Referred an old answer@ How to replace classes in a running application in java ?

Edit2: 24Aug17 Exception being faced stackTrace is here: https://pastebin.com/ckCu2uWx

Kimchy
  • 501
  • 8
  • 24
  • i feel custom classloaders could be solution to the problem but issue with that is most of the classloader examples i found only first loaded classes using custom classloader and the used reflection to invoke methods but in my case i dont want invoke methods my reflection but want pojo instance for pojo class instead for drool rule engine session. for example : http://www.concretepage.com/java/custom-classloader-java-example – Kimchy Aug 05 '17 at 08:54
  • 5
    My understanding of jsonSchema2pojo is that it generates (via JCodeModel) *java source code*, not bytecode (Did I get that wrong?). Where/when do you compile that source code? (Or is that part of the question?) – Hugues M. Aug 18 '17 at 10:47
  • Hugues thanks for replying. my workflow might be wrong but i do not have other idea how to achieve the pojo instance for json using pojo classes which are being generated at run time. Here is my workflow, my code picks up document in a elastic search index one by one further check some condition using drools drl file and re-indexes qualified documents in es index. So, for drools i need a pojo instance for testing the conditions, so as per my above code,... – Kimchy Aug 18 '17 at 17:55
  • so as per my above code,.. for each document picked from es index at runtime :- 1) json to pojo .java classes generated. 2) pojo classes are compiled at runtime and 3) custom class loader is used to load newly compiled classes to replace already loaded pojo classes. Note: As i am writing jackson TypeReferencing code , i have kept a blank pojo class(EsRootDoc) which gets loaded by default classloader into jvm and i am trying to now replace this pojo class (loaded into jvm by default class loader) with newly created EsRootDoc pojo class instance at run time. – Kimchy Aug 18 '17 at 18:02
  • At last my problem is how do i get Pojo class instance for json(deserialize json to pojo object) for run time being created pojo classes??? – Kimchy Aug 18 '17 at 18:04
  • 1
    OK thanks I think I understood, you have already taken care of compiling these generated sources and loading the classes, you only need help with how to use Jackson with the dynamically loaded classes (as said cleary in title). – Hugues M. Aug 18 '17 at 18:06
  • Please check below my earlier question posted which shows JCompiler library used to compile classes: https://stackoverflow.com/questions/45453343/jsonschema2pojo-vs-jackson-parser-conflict – Kimchy Aug 18 '17 at 18:08
  • Yeah, that my exact requirement together with correctly replacing same name pojo class in jvm. thanks a lot – Kimchy Aug 18 '17 at 18:11
  • Although keeping default blank pojo .java files in classpath and using mapper.readValue seems not a good approach but do not have better idea for getting pojo instance for deserialized json for dynamically being created pojo classes. – Kimchy Aug 18 '17 at 18:23
  • 1
    Have you tried something like `Object pojo = new ObjectMapper().readValue(json, Class.forName("com.acme.Pojo"));` ? – Hugues M. Aug 18 '17 at 18:32
  • I have tried but issue with this even if i am able to get Object reference , how would i get a pojo instance for drl validation like com.EnrichmentService.Thread72.EsRootDoc pojo instance? – Kimchy Aug 18 '17 at 18:35
  • 1
    Hi Hugues . In case of Object pojo = new ObjectMapper().readValue(json, Class.forName("com.acme.Pojo")); as well, I am still getting "com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize .." issue. As a change now, I am tried use your suggested workflow and i am not creating default blank pojo class as well and also not using pojo class import statement or fully qualified class name anywhere in my code. But for above statement getting the exception. – Kimchy Aug 21 '17 at 06:01
  • It feels if i use class.forName for same class iteratively at run time this would also need class instance replaced in jvm ? – Kimchy Aug 21 '17 at 09:42
  • I extended my test and I am very sure your code does not execute the TestClassloader. If you look into / execute my testcode from https://bitbucket.org/manuelmanhart/jackson-dyn-classes-test you will see that I get the error only if I use Class.forName() but not if I use your pasted TestClassloader – Manuel Manhart Aug 25 '17 at 15:19
  • Can you try and reproduce the incorrect behavior with my example bitbucket project? It can be run very easily and is therefor a good playground for all developers wanting to help you. – Manuel Manhart Aug 25 '17 at 15:23
  • sorry mates, I was off work and at ease at home to back to finding solution now. – Kimchy Aug 25 '17 at 16:46
  • thanks, Manuel, your code works fine on my pc as well. a further difference between your code and mine is using jsonSchema2pojo library as use of TestClassLoader is same I have been doing in my code. 1 query : your compile method compiling pojo classes and then returning new class instance doesn't really affect the process as if i change compile method return Type to void from class, still your code would work fine as you have not used than return instance anyway. – Kimchy Aug 25 '17 at 20:01
  • Cool, great it works for you now. – Manuel Manhart Aug 29 '17 at 13:26
  • Sorry, but I have not yet got time to get back to issue due to some other project requirement, I would soon test and get back here. you guys have been a great support.thanks – Kimchy Aug 30 '17 at 04:31

3 Answers3

3

you have found, you can only use TypeReference with types that are known at compile time (without some very tricky meta-programming).

However, there are lots of alternative overloads to readValue which do not require a TypeReference, to allow for cases like yours where TypeReference is impractical.

I think you can use readValue(... , Class<T> valueType)

If you have a special Classloader for these late-compiled classes, then you can get a Class instance from that and pass it in, for example:

ClassLoader dynamicallyCompiledPojoLoader = ...;
Class<?> pojoClass = dynamicallyCompiledPojoLoader.loadClass("...");

return mapper.readValue(..., pojoClass);

See also com.fasterxml.jackson.databind.type.TypeFactory for specifying parameterised generic types without using TypeReference

Update after "Edit2: 24Aug17 Exception being faced stackTrace is here"

Your current exception ( https://pastebin.com/ckCu2uWx ) is not a class loader issue, but a JSON schema mismatch issue.

The relevant part of the exception message is:

Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
...
through reference chain: com.EnrichmentService.Thread72.EsRootDoc["category"]->java.util.ArrayList[0]->com.EnrichmentService.Thread72.Category["crawler"])

So Jackson is unhappy that the "crawler" field in the JSON is an object, i.e. starts with "{", but the Java property "crawler" is an ArrayList, i.e. should start with "["

I don't know why the POJO structure of Thread72.Category is wrong here, but it doesn't look like a classloader problem.

Update after commenting that the POJO class changes with each request

You have said that 1) the POJO class structure varies with each request and 2) you want to use the same classname for the POJO each time.

See Java - how to load different versions of the same class?

You'll need to use a new Classloader for each request, as classes get cached by Classloaders. The code you have posted so far suggests that you are using a single classloader and hoping for a new load of the "Category" class on each request, which won't work.

You have said:

I need to generate new classes each time for a drools based elasticsearch document re-indexing work and this drools setup needs pojo/Object type instances.thanks

... but I think you should look into using a Map or similar input to Drools rather than a reflection based POJO, given that you don't know the structure in advance. As you have found here, this is a poor fit for the class / classloader abstraction.

See e.g.

Rich
  • 15,048
  • 2
  • 66
  • 119
  • Thanks rich but in my case i do not have a concrete pojo class as well before code exexutes. So i believe would also need a fully qualified class name say mapper.readValue(json,EsRootDoc.class); is that what you meant ? – Kimchy Aug 22 '17 at 02:21
  • Yes, something like that, or some other way of getting a Class instance. You haven't put enough detail in your question for me to be more specific. I have updated the answer. – Rich Aug 22 '17 at 12:39
  • Hi Rich, I hope if you could check the comment thread to question above if that would give you better picture. If not i would update my question. just not trying to lengthen more question but i would if not clear enough. thanks – Kimchy Aug 22 '17 at 13:08
  • Hi Rich, I have edited my answer once again please check now for custom class loader info as well. – Kimchy Aug 24 '17 at 05:15
  • I have updated my answer since you posted that stack trace – Rich Aug 24 '17 at 15:56
  • hi Rich, The reason it seems to me its a classloader issue because for each json input pojo classes (with base class EsRootDoc) are being generated correctly but as json varies say this time crawler is an object and not a list, I suddenly get this error. So pojo on the machine are correct and if these newly generated class would have been loaded correctly deserialization issue shouldn't occur. So, exception reference chain is not depicting to pojo class on machine correctly. I would let you see .java pojo classes generated by tomorrow only now as i am not at office machine right now. – Kimchy Aug 24 '17 at 17:19
  • Oh, you are generating a new Class with name "com.EnrichmentService.Thread72.Category" for each request, and the schema of the request varies each time? You'll need to use a new class name each time then, as Classloaders cache their classes. I have to say that this is a really weird setup, and I think you are well into X-Y problem territory here https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem . Why do you think you need to generate a new class for each response? – Rich Aug 24 '17 at 20:16
  • Yes, it would vary just like any other field. It would be difficult for me change class name as I am using jsonSchema2pojo library which generates pojo classes for my JSON input which are many(depending on nested document structure). As per my requirement, I need to generate new classes each time for a drools based elasticsearch document re-indexing work and this drools setup needs pojo/Object type instances.thanks – Kimchy Aug 25 '17 at 04:44
  • OK, then you need to use a new Classloader for each request. I have updated my answer – Rich Aug 25 '17 at 08:25
  • You must have seen my (edit24 aug) custom class loader and how i am trying to use, Could you suggestion some code which would work for your way of solution as i have tried mine and others like manuel's and others. – Kimchy Aug 25 '17 at 09:42
  • OK, if you are using a new Classloader each time, I can't see why that wouldn't work. Can you post enough code for us to repro the issue? – Rich Aug 25 '17 at 11:34
3

Imo there are two approaches to solve that:

  1. create & compile the classes at comile time (e.g. with maven and jaxb)

or

  1. you do something like that:

    String className = "com.EnrichmentService.Thread72.EsRootDoc";
    Class<?> clazz = Class.forName(className);
    Object object = clazz.getConstructor().newInstance();
    Object p = mapper.readValue(json, object.getClass());
    

If that code fails before mapper.readValue() you have another problem (my guess would be with classloading).

Even better would be something with generics:

    String className = "com.EnrichmentService.Thread72.EsRootDoc";
    Class<?> clazz = Class.forName(className);
    // cannot use dynamically created classes in a static way, just to 
    // show the point
    // com.EnrichmentService.Thread72.EsRootDoc p = 
    //     getObjectFromMessageString(json, clazz);
    Object p = getObjectFromString(json, clazz);

    public static <T> T getObjectFromString(String json, Class<T> clazz) {
        return mapper.readValue(json, clazz);
    }

Edit:

I wrote some example code which compiles a class on runtime and then tries to convert to an object of said compiled class. The output was as I expected:

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JackonCustomClassTest {
    public static String CLASS_NAME = "EsRootDoc";
    public static String PACKAGE_NAME = "com.EnrichmentService.Thread72";
    public static String CANONICAL_NAME = PACKAGE_NAME + "." + CLASS_NAME;

    public static void main(String args[]) throws Exception {
        JackonCustomClassTest mtc = new JackonCustomClassTest();
        Class<?> c = null;
        String source = null;
        // compile class for the first time
        source = "package "+PACKAGE_NAME+"; public class "+CLASS_NAME+" { public "+CLASS_NAME+"() { }; public String toString() { return \"Name: not existing\" + \" - className: \" + getClass().getCanonicalName(); }; }";
        c = mtc.compileClass(CANONICAL_NAME, source);

        System.out.println("class test: " + c.newInstance().toString());

        // compile class for the second time
        source = "package "+PACKAGE_NAME+"; public class "+CLASS_NAME+" { private String name; public "+CLASS_NAME+"() { }; public String getName() { return name; }; public void setName(String name) { this.name = name; }; public String toString() { return \"Name: \" + name + \" - className: \" + getClass().getCanonicalName(); }; }";
        c = mtc.compileClass(CANONICAL_NAME, source);

        System.out.println("class test: " + c.newInstance().toString());

        mtc.runJackson(c);
    }

    private void runJackson(Class<?> clazz) throws JsonParseException, JsonMappingException, IOException {
        ObjectMapper m = new ObjectMapper();
        String string = "{ \"name\": \"asdf\" }";
        Object o = m.readValue(string, clazz);
        System.out.println("result of conversion: " + o); // Should print "Name: asdf"
    }

    public Class<?> compileClass(String fullyQualifiedClassName, String source) throws Exception {
        // Save source in .java file.
        File root = new java.io.File( "./target/test-classes/" );
        File sourceFile = new File(root, fullyQualifiedClassName.replace(".", "/") + ".java");
        sourceFile.getParentFile().mkdirs();
        Files.write(sourceFile.toPath(), source.getBytes(StandardCharsets.UTF_8));

        // Compile source file.
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        compiler.run(null, null, null, sourceFile.getPath());

        // Load and instantiate compiled class.
        //          URLClassLoader classLoader = URLClassLoader.newInstance(new URL[] { root.toURI().toURL() });
        //          Class<?> cls = Class.forName(fullyQualifiedClassName, true, classLoader);
        Class<?> cls = new TestClassLoader().loadClass(fullyQualifiedClassName);
        return cls;
    }

    static class TestClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if (name.startsWith(PACKAGE_NAME)) {
                try {
                    InputStream is = this.getClass().getClassLoader()
                            .getResourceAsStream(name.replace(".",  "/") + ".class");
                    byte[] buf = new byte[is.available()];
                    int len = is.read(buf);

                    Class<?> c = defineClass(name, buf, 0, len);
                    resolveClass(c);
                    return c;

                } catch (IOException e) {
                    throw new ClassNotFoundException("", e);
                }
            }
            return getParent().loadClass(name);
        }
    }
}

Edit 2:

Updated the code to try your TestClassLoader class - still get the correct (updated) version of the class.

Manuel Manhart
  • 4,819
  • 3
  • 24
  • 28
  • Hi Manuel thanks for replying. I am still getting same error "com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance".May be due to not being able to replace already loaded class by the classloader from which it being called. Please have a look at below answer for class.forName: https://stackoverflow.com/questions/26291254/dynamically-recompile-and-reload-a-class#answer-26291430 – Kimchy Aug 22 '17 at 13:03
  • Okay. But do you load the class multiple times? Or does the class change? Because if not, I am not sure why this error occurs. Fact: The class is available, or else you would get a ClassNotFoundException or an Exception while calling the constructor. That leads me again to my two questions starting this comment. – Manuel Manhart Aug 22 '17 at 13:54
  • Yes, the EsRootDoc class gets generated iteratively at run time and class changes as well as per input json changes in each iteration. I do make sure to remove all .java files and recompile them with destination as my target folder where .class are kept in my project.So, all .java classes and .class are replaced for each iteration. – Kimchy Aug 22 '17 at 15:00
  • Hmm, did you think about using Rhino or Nashorn (JS Engines for Java) for processing your data? There you would have dynamic typings and would not need the dynamic classes. – Manuel Manhart Aug 22 '17 at 21:34
  • Maybe your codeModel could return the Class? You could then put it in the ObjectMapper and it should be up to date (much like my last example code). I will think about the problem a bit more. – Manuel Manhart Aug 22 '17 at 21:37
  • Thanks for showing such a commitment, but I have a query regarding your code (no disregard to your skills!), could you run the same code in a for loop and vary your Test class to test whether your code detects class variation. Please check your code variation here : https://pastebin.com/xqHyd5UX – Kimchy Aug 23 '17 at 12:50
  • Hi Manuel, I have edited my answer once again please check now for custom class loader info as well. – Kimchy Aug 24 '17 at 05:15
  • Hi. I did not create a loop but created the test class twice - I added the name function only in the 2nd version of the class - and it works. I will have a look into your updated code - my guess is that it works in my code because of the "URLClassLoader.newInstance" line combined with passing the classLoader into the forName call. – Manuel Manhart Aug 24 '17 at 09:39
  • Hi Manuel, You code works fine for me as well but URLClassLoader also not working for me. I feel the reason for it being JSON input to be serialized is too nested in my case. Here is my sample JSON as being used input: https://pastebin.com/3qwsRe0b So, for JSON input multiple linked POJO classes gets generated using jsonSchema2Pojo library. – Kimchy Aug 24 '17 at 10:53
  • The varying JSON input is here: https://pastebin.com/6T361ViU Change in 2 JSONs :Please check variation with category.nlp.mainCategory variation – Kimchy Aug 24 '17 at 11:15
  • Okay, I think I was now able to reproduce your problem when I deserialize the first, recompile the (sub)class and then deserialize the second - I get this exception: Can not deserialize instance of com.EnrichmentService.Thread72.Nlp out of START_ARRAY token. Is this what you get too? – Manuel Manhart Aug 24 '17 at 12:25
  • exactly, Manuel, that's deserialization issue I am facing. – Kimchy Aug 25 '17 at 04:48
  • Okay, thats great. As we now have a simplified console program to reproduce the problem I am sure we can come up with a solution. – Manuel Manhart Aug 25 '17 at 10:27
1

But as com.EnrichmentService.Thread72.EsRootDoc has yet not been generated compiler would error to class not Found.

Yes, of course. If you don't have class in your JVM, you can't load them.

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize

Please, give all stacktrace.

1) Same Pojo classes generated at run time iteratively but with different properties as JSON input changes each time.

Why you don't use Maps? Why you don't use one big class with all fields (and some of them will be nulls)?

Yes, the EsRootDoc class gets generated iteratively at run time and class changes as well as per input json changes in each iteration

If you do this in multiple threads, just synchronized them, for example:

final String ConcurrentHashMap<String, Class> dynamicClasses = new ConcurrentHashMap();

Object doInThreadOne(String json, String className) {
    return mapper.readObject(json, dynamicClasses.get(className))

void doInAnotherThread(String className) {
    dynamicClasses.put(className, reload(className));
}

If you need more strong consistency, then you can use synchronization by class name:

static final String className = "com.EnrichmentService.Thread72.EsRootDoc";

final String Map<String, Class> dynamicClasses = new HashMap();

Object doInThreadOne(String json) {
    synchronized(className) {
        return mapper.readObject(json, dynamicClasses.get(className))
    }

void doInAnotherThread(String className) {
    synchronized(className) {
        dynamicClasses.put(className, reload(className));
    }
}

Method Class.forName uses class loader of caller. You use different classloaders, it could be reason. There are overloads, where you can pass classloader.

Update for stackrace and additional information.

You should add @JsonIgnoreProperties(ignoreUnknown = true) to Crawler, because it has field subCategory, which is not present in Pojo.

egorlitvinenko
  • 2,736
  • 2
  • 16
  • 36
  • Hi egorlitvinenko, 1) stacktrace: https://pastebin.com/ckCu2uWx 2) For nested JSON input, the jsonSchema2Pojo library generates multiple POJO classes. And I have a drools POJO based workflow for which I need POJOs. 3) Thanks for multi threaded issue suggestion, this would be helpful soon than later. – Kimchy Aug 24 '17 at 11:33
  • Did you load Crawler class? – egorlitvinenko Aug 24 '17 at 12:00
  • Generally, it looks like your POJO does not match JSON. Could you also add generated POJO? – egorlitvinenko Aug 24 '17 at 12:03
  • https://pastebin.com/gP7V7xQp These should be POJO classes generated for JSON https://pastebin.com/CBJmWVXa. My this comment would also mean this JSON is just one of the variation and pastebin codes is to give a feel of nested structures inputs are. – Kimchy Aug 24 '17 at 12:17
  • Add @JsonIgnoreProperties(ignoreUnknown = true) to Crawler – egorlitvinenko Aug 24 '17 at 12:30
  • You have subCategory field in json. – egorlitvinenko Aug 24 '17 at 12:30
  • @user3152549 did you try? – egorlitvinenko Aug 24 '17 at 16:21
  • I am actually not doing POJO generation on my own but POJO classes are being generated by jsonSchema2pojo library (http://www.jsonschema2pojo.org). I would try to find some way to enable ingnoreUnknown=true which has to be done at runtime. – Kimchy Aug 24 '17 at 16:34