javagenericsjacksonhashmappolymorphism

Jackson (de-) serialization hashmap with different object types


I am currently programming a game in Java, and I am having a problem with saving the entities data in Jackson. I need to (de-) serialize a HashMap with different subclasses of a superclass. E.g. I have a class called Mob and every entity, like Player for example, is a subclass of Mob:

public HashMap<Byte, Mob> mobs = new Hashmap<>(); // Byte is ok here, as I don't have many mobs yet and it can also be changed to whatever I need to

public class Player extends Mob {} // The Player

How I want to serzialize the map:

public void loadEntities(String path) {
        ObjectMapper om = new ObjectMapper();

        try {
            runThread.mobs = om.readValue(new File(path + "/entities.json"), HashMap.class);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

But what do I need to do, in order for the HashMap to deserzialize the mob player in the JSON-file correctly and later serialize it back as a player into the HashMap, instead of a normal mob-object?

What my JSON could look like:

{
  "0" : {
    "posX" : 256,
    "posY" : 128,
    "rotation" : "links"
  },

  "2" : {
    "posX" : 128,
    "posY" : 256,
    "rotation" : "rechts"
  }
}

The 0 and the 2 are the IDs of the entities, but I can change the HashMap to a list if needed.

Full example of my mob:


import de.pki.GUI.Welt.Tiles.Tiles;

import java.awt.image.BufferedImage;

public abstract class Mob {
    public float posX;
    public float posY;

    public float screenX;
    public float screenY;

    // Physik

    public float forceX;
    public float forceY;

    public float gravity = 9.81F;
    public float currentGravity = gravity * 2;

    public float rotation; // Für Projektile, welche sich in der Luft drehen müssen
    public String richtung; // Für normale Mobs, welche nur zwei Richtungen brauchen
    public boolean vectorAble; // TODO

    public float standardSpeed;
    public float sprintSpeed;
    public float currentSpeed;

    public byte maxSpruenge = 2;
    public byte spruengeGenutzt;
    public float sprungKraft;
    public float aktuelleSprungkraft = -5;
    public float lastJump = System.currentTimeMillis();

    // Hit box

    public short startBoxX;
    public short startBoxY;
    public short endBoxX;
    public short endBoxY;

    public float opacity;

    // Bilder

    public BufferedImage idleLinks;
    public BufferedImage idleRechts;

    public BufferedImage[] links;
    public BufferedImage[] rechts;
    public BufferedImage currentImg;

    public byte groesse = 1;

    public byte imageCount = 0;
    public double lastImageAnimation = System.currentTimeMillis();

    // Methoden
    // Physik

    public void updateForces(CollisionChecker collisionChecker, int tickSpeed) {
        if (forceX > 0) {
            collisionChecker.checkRunning((int) (posX + startBoxX + endBoxX), forceX / tickSpeed, 1, this);
        } else if (forceX < 0) {
            collisionChecker.checkRunning((int) (posX + startBoxX - 4), (forceX * - 1) / tickSpeed, -1, this);
        }

        if (forceY > 0) {
            collisionChecker.checkFalling((int) (posY + startBoxY + endBoxY), forceY / tickSpeed, 1, this);
        } else if (forceY < 0) {
            collisionChecker.checkFalling((int) (posY + startBoxY - 2), (forceY * - 1) / tickSpeed, -1, this);
        }
    }

    public void gravity(double tickSpeed) {
        forceY += (float) (currentGravity / tickSpeed);
    }

    public void jump() {
        if (lastJump + 500 < System.currentTimeMillis()) {
            if (spruengeGenutzt < maxSpruenge) {
                forceY = aktuelleSprungkraft;
                spruengeGenutzt++;

                lastJump = System.currentTimeMillis();
            }
        }
    }

    // Für Objekte, welche Vektoren besitzen

    public void changeVektor(Mob mob, Tiles tiles, double aufprallWinkel) {
        if (mob.vectorAble) {}

        // TODO: Noch hinzufügen
    }

    // Grafische Sachen

    public void updateImages() {
        switch (richtung) {
            case "rechts" -> {
                if (System.currentTimeMillis() - lastImageAnimation >= (double) 60 / rechts.length / 1000 + 150) {
                    if (imageCount + 1 >= rechts.length) {
                        imageCount = 0;
                    } else {
                        imageCount++;
                    }

                    currentImg = rechts[imageCount];
                    lastImageAnimation = System.currentTimeMillis();
                }
            }
            case "links" -> {
                if (System.currentTimeMillis() - lastImageAnimation >= (double) 60 / links.length / 1000 + 150) {
                    if (imageCount + 1 >= links.length) {
                        imageCount = 0;
                    } else {
                        imageCount++;
                    }

                    currentImg = links[imageCount];
                    lastImageAnimation = System.currentTimeMillis();
                }
            }
        }

        if (forceX == 0) {
            if (richtung.equals("rechts")) {
                currentImg = idleRechts;
            } else if (richtung.equals("links")) {
                currentImg = idleLinks;
            }
        }
    }
}

And my player:

package de.pki.Entities.Spieler;

import de.pki.Entities.Mob;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class Spieler extends Mob {
    public Spieler() {
        loadVariables();
        loadImages();

        hitBoxRechts();
    }

    private void loadImages() {
        try {
            links = new BufferedImage[2];
            rechts = new BufferedImage[2];

            idleLinks = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/Idle/left_idle.png"));
            idleRechts = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/Idle/right_idle.png"));

            links[0] = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/WalkingAnimation/linksLauf1.png"));
            links[1] = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/WalkingAnimation/linksLauf2.png"));

            rechts[0] = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/WalkingAnimation/rechtsLauf1.png"));
            rechts[1] = ImageIO.read(new FileInputStream("src/Data/Images/Entities/Spieler/WalkingAnimation/rechtsLauf2.png"));

            currentImg = idleRechts;
        } catch (IOException e) {
            System.out.println(e);
        }
    }

    private void loadVariables() {
        sprungKraft = (float) (-9.81 * 1);
        aktuelleSprungkraft = sprungKraft;
        maxSpruenge = 2;
        spruengeGenutzt = 0;

        standardSpeed = 3;
        sprintSpeed = 4.5F;
        currentSpeed = standardSpeed;

        richtung = "rechts";

        groesse = 2;

        hitBoxRechts();
    }

    public void hitBoxRechts() {
        startBoxX = (short) (6 * groesse);
        startBoxY = (short) (3 * groesse);
        endBoxX = (short) (19 * groesse);
        endBoxY = (short) (23 * groesse);
    }
}```

(I'm currently in class, so it took me some time to edit the code)
Thanks for the help

Solution

  • Serializing/Deserializing Polymorphic Instances

    To properly serialize polymorphic instances, you need to use the annotations JsonTypeInfo and JsonSubTypes. Basically, JsonTypeInfo allows you to define an extra property when serializing an instance of the annotated class, while JsonSubTypes defines the value of that extra property for each subclass. In your case, it would look something like this:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "class")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Player.class, name = "player")
    })
    public abstract class Mob {
       //... implementation ...
    }
    
    public class Player extends Mob {
       //... implementation ...
    }
    

    So, your Json would be something like:

    {
        "0": {
            "posX": 1.0,
            "posY": 2.0,
            "rotation": "links",
            "class": "player"
        },
        "2": {
            "posX": 3.0,
            "posY": 4.0,
            "rotation": "rechts",
            "class": "player"
        }
    }
    

    Of course, JsonTypeInfo and JsonSubTypes can be used for multiple subclasses, and for each one of them you just need to define their corresponding @JsonSubTypes.Type. For example, if there was an Enemy class, it would be something like this:

    @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "class")
    @JsonSubTypes({
            @JsonSubTypes.Type(value = Player.class, name = "player"),
            @JsonSubTypes.Type(value = Enemy.class, name = "enemy")
    })
    public abstract class Mob {
       //... implementation ...
    }
    
    public class Player extends Mob {
       //... implementation ...
    }
    
    public class Enemy extends Mob {
       //... implementation ...
    }
    

    Here is also an interesting article from Baeldung covering the topic.

    Deserializing Generic Types

    Since you want to store your Player instances in a generic data structure, like Map or List, you need to use a TypeReference subclassing your data structure, so that Jackson knows how to properly deserialize the json.

    ObjectMapper mapper = new ObjectMapper();
    Map<Byte, Mob> map = mapper.readValue(json, new TypeReference<Map<Byte, Mob>>() {});
    

    Demo of your Code

    Here, is a link to OneCompiler with a demo of your code. As you can see from the output, even though it is used a Map<Byte, Mob> to read the json, the instances within the Map are Player objects, which answers your question:

    But what do I need to do, in order for the HashMap to deserzialize the mob player in the JSON-file correctly and later serialize it back as a player into the HashMap, instead of a normal mob-object?