private void serialize(
      final @CheckForNull Type declaredType,
      @CheckForNull MetaData declaredMetaData,
      @CheckForNull GenericValue value,
      final ByteWriter destination) {
    boolean optional = (declaredType == null || declaredType.isOptional());
    boolean polymorphic =
        (declaredMetaData == null || declaredMetaData.getMetaType().isPolymorphic());
    if (value == null) {
      if (!optional) {
        throw new SerializerException("Non-optional entry '" + entryId + "' found to be null!");
      }
      if (polymorphic) {
        serializerRepository
            .getEntityIdSerializer()
            .serialize(BootStrap.ID_NULL_REFERENCE, destination);
      } else {
        destination.putVarInt(0); // TODO optimize
      }
      return;
    }
    if (polymorphic) {
      if (declaredMetaData == null || declaredMetaData.getMetaType() != MetaType.ENTITY) {
        // Polymorphic type: Write actual type or 'null'-type.
        serializerRepository
            .getEntityIdSerializer()
            .serialize(value.getActualMetaDataId(), destination);
      }
    } else {
      // Non-polymorphic type: Do not write actual type.
      if (optional) {
        // As we do not serialize the type if the declared type is not
        // polymorphic we need to serialize whether it is present or
        // not.
        destination.putVarInt(1); // TODO optimize
      }
    }

    // TODO check that type matches declared type
    // TODO replace ValueVisitor by switch over MetaType for symmetry with
    // deserialize?
    // Or replace MetaData with subclasses (probably better).
    value.accept(
        new ValueVisitor() {
          @Override
          public void visit(PrimitiveValue primitive) {
            BinarySerializer<Object> serializer =
                serializerRepository.getPrimitiveSerializer(primitive.getActualMetaDataId());
            serializer.serialize(primitive.getValue(), destination);
          }

          @Override
          public void visit(ValueObjectValue valueObject) {
            BinarySerializer<ObjectInfo> serializer =
                serializerRepository.getBinarySerializer(valueObject.getActualMetaDataId());
            serializer.serialize(valueObject.getValue(), destination);
          }

          @Override
          public void visit(EntityIdValue entityReference) {
            serializerRepository
                .getEntityIdSerializer()
                .serialize(entityReference.getEntityId(), destination);
          }

          @Override
          public void visit(CollectionValue collection) {
            // Write element MetaDataId.
            serializerRepository
                .getEntityIdSerializer()
                .serialize(collection.getValueMetaDataId(), destination);
            // Write element count.
            destination.putVarInt(collection.getCount());
            // Write custom collection implementation data.
            BinarySerializer<ObjectInfo> serializer =
                serializerRepository.getBinarySerializer(collection.getActualMetaDataId());
            serializer.serialize(collection.getCollectionInfo(), destination);
            // Write all elements.
            for (GenericValue element : collection.getValues()) {
              Type elementType = (declaredType == null) ? null : declaredType.getElementType();
              MetaData metaData =
                  (elementType == null)
                      ? null
                      : serializerRepository.getMetaData(elementType.getMetaDataId());
              if (element == GenericValue.nullValue()) {
                element = null;
              }
              serialize(elementType, metaData, element, destination);
            }
          }

          @Override
          public void visit(NullValue nullValue) {
            throw new IllegalArgumentException("Cannot deserialize NullValue!");
          }
        });
  }
  @CheckForNull
  private GenericValue deserializeValue(
      ByteReader source, @CheckForNull Type declaredType, @CheckForNull MetaData declaredMetaData) {
    boolean optional = (declaredType == null || declaredType.isOptional());
    MetaData actualMetaData = null;
    if (declaredMetaData == null || declaredMetaData.getMetaType().isPolymorphic()) {
      if ((declaredMetaData != null) && (declaredMetaData.getMetaType() == MetaType.ENTITY)) {
        actualMetaData = BootStrap.ENTITY_ID;
      } else {
        // Polymorphic type: Read actual type or 'null'-type.
        EntityId actualTypeId = serializerRepository.getEntityIdSerializer().deserialize(source);
        if (!BootStrap.ID_NULL_REFERENCE.equals(actualTypeId)) {
          actualMetaData = serializerRepository.getMetaData(actualTypeId);
        }
      }
    } else {
      // Non-polymorphic type: Do not read actual type.
      if (!optional || source.getVarInt() != 0) {
        actualMetaData = declaredMetaData;
      }
    }

    if (actualMetaData == null) {
      if (!optional) {
        throw new SerializerException("Non-optional entry found to be null!");
      }
      return null;
    }

    switch (actualMetaData.getMetaType()) {
      case ENTITY:
        {
          EntityId entityId = serializerRepository.getEntityIdSerializer().deserialize(source);
          return BootStrap.ID_NULL_REFERENCE.equals(entityId) ? null : new EntityIdValue(entityId);
        }
      case PRIMITIVE:
        {
          Primitive primitive = Primitive.byEntityId(actualMetaData.getEntityId());
          BinarySerializer<?> serializer =
              serializerRepository.getPrimitiveSerializer(actualMetaData.getEntityId());
          return new PrimitiveValue(primitive, serializer.deserialize(source));
        }
      case VALUE_OBJECT:
        {
          BinarySerializer<ObjectInfo> serializer =
              serializerRepository.getBinarySerializer(actualMetaData.getEntityId());
          return new ValueObjectValue(serializer.deserialize(source));
        }
      case COLLECTION:
        {
          // Read element MetaDataId.
          EntityId elementMetaDataId =
              serializerRepository.getEntityIdSerializer().deserialize(source);
          // Read element count.
          int count = MathUtil.longToInt(source.getVarInt());
          // Read custom collection implementation data.
          BinarySerializer<ObjectInfo> serializer =
              serializerRepository.getBinarySerializer(actualMetaData.getEntityId());
          ObjectInfo collectionInfo = serializer.deserialize(source);
          // Read all elements.
          ImmutableArrayList.Builder<GenericValue> builder = ImmutableArrayList.newBuilder(count);
          for (int i = 0; i < count; ++i) {
            Type elementType = (declaredType == null) ? null : declaredType.getElementType();
            MetaData metaData =
                (elementType == null)
                    ? null
                    : serializerRepository.getMetaData(elementType.getMetaDataId());
            GenericValue value = deserializeValue(source, elementType, metaData);
            builder.add((value == null) ? GenericValue.nullValue() : value);
          }
          return new CollectionValue(collectionInfo, elementMetaDataId, builder.build());
        }
      default:
        throw new IllegalStateException("Unknown MetaType " + actualMetaData.getMetaType());
    }
  }