Dennis van der Veeke
Dennis van der Veeke

Reputation: 854

Gson Java - Child class from JSON

I have an abstract class for configuration files, which can be extended by lots of other classes. I managed to get the system working for writing it to JSON, but now I need the load function.

Here's the general Configuration class:

public class Configuration {

    public boolean load(){
        FileReader reader = new FileReader(this.getClass().getSimpleName() + ".json");
        Gson gson = new Gson();
        gson.fromJson(reader, this.getClass());
        reader.close();
        /** Doesn't give an error, but doesn't set any info to the child class */
    }

    public boolean save(){
        FileWriter writer = new FileWriter(this.getClass().getSimpleName() + ".json");
        Gson gson = new Gson();
        gson.toJson(this, writer);
        writer.close();
        /** This all works fine. */
    }

}

Here's an example of an extending class:

public class ExampleConfig extends Configuration {

    private static transient ExampleConfig i = new ExampleConfig();
    public static ExampleConfig get() { return i; }

    @Expose public String ServerID = UUID.randomUUID().toString();

}

In my main class I would do:

ExampleConfig.get().load();
System.out.println(ExampleConfig.get().ServerID);

This does not give any errors, but neither is the class loaded from the JSON. It keeps outputting a random UUID even though I want to load one from the JSON file. I'm probably getting the wrong instance of the child class, but I'm out of ideas on how to fix this. (Using this in gson.fromJson(.....); does not work.

Upvotes: 1

Views: 1270

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

You're missing to assign a read value to your configuration instance. Java cannot support anything like this = gson.fromJson(...), and Gson can only return new values and cannot patch existing ones. The below is a sort of Gson hack, and please only use it if it's really a must for you. Again, I would strongly recommend you to redesign your code and separate your configuration objects and configuration readers/writers -- these are just two different things that conflict from the technical perspective. As a result of refactoring, you could have, let's say, once you get an instance of your configuration, just delegate it to a writer to persist it elsewhere. If you need it back, then just get an instance of a reader, read the configuration value and assign it to your configuration (configurations are singletons, I remember), like:

final ConfigurationWriter writer = getConfigurationWriter();
writer.write(ExampleConfig.get());
...
final ConfigurationReader reader = getConfigurationReader();
ExampleConfig.set(reader.read(ExampleConfig.class));

At least this code does not mix two different things, and makes the result of reader.read be explicitly read and assigned to your configuration singleton.

If you're fine to open the gate of evil and make your code work because of hacks, then you could use Gson TypeAdapterFactory in order to cheat Gson and patch the current configuration instance.

abstract class Configuration {

    private static final Gson saveGson = new Gson();

    public final void load()
            throws IOException {
        try ( final FileReader reader = new FileReader(getTargetName()) ) {
            // You have to instantiate Gson every time (unless you use caching strategies) in order to let it be *specifically* be aware of the current
            // Configuration instance class. Thus you cannot make it a static field.
            final Gson loadGson = new GsonBuilder()
                    .registerTypeAdapterFactory(new TypeAdapterFactory() {
                        // A Gson way to denote a type since Configuration.class may not be enough and it also works with generics
                        private final TypeToken<Configuration> configurationTypeToken = new TypeToken<Configuration>() {
                        };

                        @Override
                        @SuppressWarnings("deprecation") // isAssignableFrom is deprecated
                        public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
                            // Checking if the type token represents a parent class for the given configuration
                            // If yes, then we cheat...
                            if ( configurationTypeToken.isAssignableFrom(typeToken) ) {
                                // The map that's artificially bound as great cheating to a current configuration instance
                                final Map<Type, InstanceCreator<?>> instanceCreators = bindInstance(typeToken.getType(), Configuration.this);
                                // A factory used by Gson internally, we're intruding into its heart
                                final ConstructorConstructor constructorConstructor = new ConstructorConstructor(instanceCreators);
                                final TypeAdapterFactory delegatedTypeAdapterFactory = new ReflectiveTypeAdapterFactory(
                                        constructorConstructor,
                                        gson.fieldNamingStrategy(),
                                        gson.excluder(),
                                        new JsonAdapterAnnotationTypeAdapterFactory(constructorConstructor)
                                );
                                // Since the only thing necessary here is to define how to instantiate an object
                                // (and we just give it an already-existing instance)
                                // ... just delegate the job to Gson -- it would think as if it's creating a new instance.
                                // Actually it won't create one, but would "patch" the current instance
                                return delegatedTypeAdapterFactory.create(gson, typeToken);
                            }
                            // Otherwise returning a null means looking up for an existing type adapter from how Gson is configured
                            return null;
                        }
                    })
                    .create();
            // The value is still loaded to nowhere, however.
            // The type adapter factory is tightly bound to an existing configuration instance via ConstructorConstructor
            // This is actually another code smell...
            loadGson.fromJson(reader, getClass());
        }
    }

    public final void save()
            throws IOException {
        try ( final FileWriter writer = new FileWriter(getTargetName()) ) {
            saveGson.toJson(this, writer);
        }
    }

    private String getTargetName() {
        return getClass().getSimpleName() + ".json";
    }

    private static Map<Type, InstanceCreator<?>> bindInstance(final Type type, final Configuration existingConfiguration) {
        return singletonMap(type, new InstanceCreator<Object>() {
            @Override
            public Object createInstance(final Type t) {
                return t.equals(type) ? existingConfiguration : null; // don't know if null is allowed here though
            }
        });
    }

}

I hope that the comments in the code above are exhaustive. As I said above, I doubt that you need it just because of intention to have a bit nicer code. You could argue that java.util.Properties can load and save itself. Yes, that's true, but java.util.Properties is open to iterate over its properties by design and it can always read and write properties from elsewhere to anywhere. Gson uses reflection, a method of peeking the fields under the hood, and this is awesome for well-designed objects. You need some refactoring and separate two concepts: the data and data writer/reader.

Upvotes: 1

Related Questions