junkdog / artemis-odb

A continuation of the popular Artemis ECS framework
BSD 2-Clause "Simplified" License
777 stars 112 forks source link

[Question] Why does JsonArtemisSerializer fail to serialize int and boolean primitives that are 0 and false? #623

Closed bryanbrunt closed 3 years ago

bryanbrunt commented 3 years ago

JsonArtemisSerializer just doesn't serialize int and boolean fields on components that are 0 and false.

Why?

The potentially counter argument against serializing these zero and false primitive fields is that when those fields de-serialize they will be set to zero and false anyway.

However, if you have a component like below with an int index = 0, then the serializer will not serialize the component. The serializer essentially deletes this data.

When you go to deserialize, the component doesn't exist.

This is a loss of data issue. An index of 0 for this component is possibly valid.

public class InventoryPosition extends Component { public int index; }

junkdog commented 3 years ago

However, if you have a component like below with an int index = 0, then the serializer will not serialize the component. The serializer essentially deletes this data.

Any default-valued primitive fields (and string fields) are omitted from the json as they're set to the default value automatically upon instantiating the component.

    @Test
    public void save_component_with_default_values() throws Exception {
        int defaultIndex = 0;
        int customIndex = 4;

        EntityEdit ee1 = world.createEntity().edit();
        ee1.create(InventoryPosition.class).index = defaultIndex; // default value
        ee1.create(SerializationTag.class).tag = "field with default value";
        EntityEdit ee2 = world.createEntity().edit();
        ee2.create(InventoryPosition.class).index = customIndex; // custom value
        ee2.create(SerializationTag.class).tag = "field with custom value";
        world.process();

        EntitySubscription inventories = subscriptions.get(all(InventoryPosition.class));
        String json = save(inventories, wsm);

        deleteAll();

        ByteArrayInputStream is = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));
        SaveFileFormat l = wsm.load(is, SaveFileFormat.class);

        world.process();

        assertEquals(2, inventories.getEntities().size());
        assertEquals(defaultIndex, l.get("field with default value")
                .getComponent(InventoryPosition.class).index);
        assertEquals(customIndex, l.get("field with custom value")
                .getComponent(InventoryPosition.class).index);
    }

The saved json looks as follows:

{
"metadata": {
    "version": 1
},
"componentIdentifiers": {
    "com.artemis.component.InventoryPosition": "InventoryPosition"
},
"entities": {
    "3": {
        "archetype": 9,
        "key": "field with default value",
        "components": {}
    },
    "4": {
        "archetype": 9,
        "key": "field with custom value",
        "components": {
            "InventoryPosition": {
                "index": 4
            }
        }
    }
},
"archetypes": {
    "9": [
        "InventoryPosition"
    ]
}
}

Both entities point to the same archetype 9 (the archetype is the same as the compositionId), which contains the InventoryPosition component.

If you're processing the json in something external, where default values aren't known, there's always:

// don't omit default-valued fields
((JsonArtemisSerializer) wsm.getSerializer()).setUsePrototypes(false);
DaanVanYperen commented 3 years ago

A workaround has been provided. Closing as answered.