You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by ah...@apache.org on 2022/09/02 08:04:48 UTC

[isis] 01/01: ISIS-3200: ManagedObjects of type VALUE should provide bookmarks themselves

This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch 3200_broken-value-choices
in repository https://gitbox.apache.org/repos/asf/isis.git

commit 4174060ff1a11d87e14677bdc7fbf93894934ef0
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Sep 2 10:04:38 2022 +0200

    ISIS-3200: ManagedObjects of type VALUE should provide bookmarks
    themselves
    
    - also introduce URL_SAFE semantics in the context of value serializers
---
 .../isis/applib/services/bookmark/Bookmark.java    |  14 +-
 .../images/managed-object-diagram.drawio.svg       |   2 +-
 .../facets/object/entity/EntityFacet.java          |  36 ++++++
 .../facets/object/value/ValueFacetAbstract.java    |  10 +-
 .../facets/object/value/ValueSerializer.java       |   9 +-
 .../object/value/ValueSerializerDefault.java       |  32 +++--
 .../object/viewmodel/ViewModelFacetAbstract.java   |   4 +-
 .../isis/core/metamodel/object/ManagedObject.java  |  14 +-
 .../isis/core/metamodel/object/ManagedObjects.java |  11 +-
 .../metamodel/object/_ManagedObjectService.java    |   4 +-
 .../core/metamodel/object/_ManagedObjectValue.java |   9 +-
 .../metamodel/objectmanager/ObjectManager.java     |  18 +--
 .../objectmanager/identify/ObjectBookmarker.java   |   1 +
 .../identify/ObjectBookmarker_builtinHandlers.java |  70 +++++-----
 .../metamodel/spec/HasObjectSpecification.java     |  60 +++++++++
 .../core/metamodel/util/snapshot/XmlSnapshot.java  |  26 ++--
 .../value/JavaTimeValueSemanticsProviderTest.java  |  11 +-
 .../ValueSemanticsProviderAbstractTestCase.java    |  41 +++---
 .../isis/core/runtime/IsisModuleCoreRuntime.java   |   4 +-
 ...ervice.java => IdStringifierLookupService.java} |  42 ++----
 .../command/CommandDtoFactoryDefault.java          |   3 +-
 .../interaction/InteractionDtoFactoryDefault.java  |  13 +-
 .../memento/ObjectMementoServiceDefault.java       |   6 +-
 .../runtimeservices/memento/_ObjectMemento.java    | 144 +++++++++------------
 .../entities/DnEntityStateProvider.java            |  10 +-
 .../metamodel/facets/entity/JdoEntityFacet.java    |  41 ++++--
 .../jpa/integration/entity/JpaEntityFacet.java     |  32 +++--
 .../integration/entity/JpaEntityFacetFactory.java  |   2 +-
 .../JsonValueEncoderServiceDefault.java            |   4 +-
 .../wicket/model/util/PageParameterUtils.java      |  16 ---
 30 files changed, 377 insertions(+), 312 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/bookmark/Bookmark.java b/api/applib/src/main/java/org/apache/isis/applib/services/bookmark/Bookmark.java
index c23d21eb6c..5d7036628a 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/bookmark/Bookmark.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/bookmark/Bookmark.java
@@ -75,19 +75,19 @@ public final class Bookmark implements Oid {
 
     public static Bookmark forLogicalTypeNameAndIdentifier(
             final @NonNull String logicalTypeName,
-            final @NonNull String identifier) {
+            final @NonNull String urlSafeIdentifier) {
         return new Bookmark(
                 logicalTypeName,
-                identifier,
+                urlSafeIdentifier,
                 /*hintId*/null);
     }
 
     public static Bookmark forLogicalTypeAndIdentifier(
             final @NonNull LogicalType logicalType,
-            final @NonNull String identifier) {
+            final @NonNull String urlSafeIdentifier) {
         return Bookmark.forLogicalTypeNameAndIdentifier(
                 logicalType.getLogicalTypeName(),
-                identifier);
+                urlSafeIdentifier);
     }
 
     public static Bookmark forOidDto(final @NonNull OidDto oidDto) {
@@ -104,12 +104,12 @@ public final class Bookmark implements Oid {
 
     private Bookmark(
             final String logicalTypeName,
-            final String identifier,
+            final String urlSafeIdentifier,
             final String hintId) {
         this.logicalTypeName = logicalTypeName;
-        this.identifier = identifier;
+        this.identifier = urlSafeIdentifier;
         this.hintId = hintId;
-        this.hashCode = Objects.hash(logicalTypeName, identifier);
+        this.hashCode = Objects.hash(logicalTypeName, urlSafeIdentifier);
     }
 
     // -- PARSE
diff --git a/core/metamodel/src/main/adoc/modules/metamodel/images/managed-object-diagram.drawio.svg b/core/metamodel/src/main/adoc/modules/metamodel/images/managed-object-diagram.drawio.svg
index bd01036df0..55aa26123a 100644
--- a/core/metamodel/src/main/adoc/modules/metamodel/images/managed-object-diagram.drawio.svg
+++ b/core/metamodel/src/main/adoc/modules/metamodel/images/managed-object-diagram.drawio.svg
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- Do not edit this file with editors other than diagrams.net -->
 <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1391px" height="591px" viewBox="-0.5 -0.5 1391 591" content="&lt;mxfile host=&quot;Electron&quot; modified=&quot;2022-09-01T14:29:56.898Z&quot; agent=&quot;5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.2.3 Chrome/102.0.5005.167 Electron/19.0.11 Safari/537.36&quot; etag=&quot;cwu7jfOeW0fUMNdx5SSP&quot; version=&quot;20.2.3&quot; type=&quot;device&qu [...]
\ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1391px" height="591px" viewBox="-0.5 -0.5 1391 591" content="&lt;mxfile host=&quot;Electron&quot; modified=&quot;2022-09-02T03:02:42.988Z&quot; agent=&quot;5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.2.3 Chrome/102.0.5005.167 Electron/19.0.11 Safari/537.36&quot; etag=&quot;lW1AIfm3Z-gm0gO0WKht&quot; version=&quot;20.2.3&quot; type=&quot;device&qu [...]
\ No newline at end of file
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/entity/EntityFacet.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/entity/EntityFacet.java
index f3537fb078..9daafec0df 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/entity/EntityFacet.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/entity/EntityFacet.java
@@ -20,13 +20,17 @@ package org.apache.isis.core.metamodel.facets.object.entity;
 
 import java.lang.reflect.Method;
 import java.util.Optional;
+import java.util.function.Function;
 
 import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
 
 import org.apache.isis.applib.query.Query;
 import org.apache.isis.applib.services.bookmark.Bookmark;
+import org.apache.isis.applib.services.bookmark.IdStringifier;
 import org.apache.isis.applib.services.repository.EntityState;
 import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.internal.base._Casts;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.config.beans.PersistenceStack;
 import org.apache.isis.core.metamodel.facetapi.Facet;
@@ -35,12 +39,44 @@ import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.object.MmSpecUtil;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 
+import lombok.NonNull;
+
 /**
  * Indicates that this class is managed by a persistence context.
  * @since 2.0
  */
 public interface EntityFacet extends Facet {
 
+    @lombok.Value(staticConstructor = "of")
+    static class PrimaryKeyType<T> {
+        private final @NonNull Class<?> owningEntityClass;
+        private final @NonNull IdStringifier<T> idStringifier;
+        private final @NonNull Class<T> primaryKeyClass;
+        public String enstring(final T primaryKey) {
+            return idStringifier.enstring(primaryKey);
+        }
+        public String enstringWithCast(final Object primaryKey) {
+            return _Casts.castTo(primaryKeyClass, primaryKey)
+            .map(idStringifier::enstring)
+            .orElseThrow(()->_Exceptions.illegalArgument(
+                    "failed to cast primary-key '%s' to expected type %s",
+                        ""+primaryKey,
+                        primaryKeyClass.getName()));
+        }
+        public T destring(final String stringifiedPrimaryKey) {
+            return idStringifier.destring(owningEntityClass, stringifiedPrimaryKey);
+        }
+        public static <T> PrimaryKeyType<T> getInstance(
+                final @NonNull Class<?> owningEntityClass,
+                final @NonNull Function<Class<T>, IdStringifier<T>> stringifierLookup,
+                final @NonNull Class<T> primaryKeyClass){
+            return of(
+                    owningEntityClass,
+                    stringifierLookup.apply(primaryKeyClass),
+                    _Casts.uncheckedCast(ClassUtils.resolvePrimitiveIfNecessary(primaryKeyClass)));
+        }
+    }
+
     /**
      * The {@link ObjectSpecification} of the entity type this
      * facet is associated with.
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueFacetAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueFacetAbstract.java
index 3c402b246a..9ba54f5acb 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueFacetAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueFacetAbstract.java
@@ -132,16 +132,16 @@ implements ValueFacet<T> {
                 iaProvider.currentInteractionContext().orElse(null));
     }
 
-    // -- TO STRING SERIALIZATION
+    // -- TO/FROM STRING SERIALIZATION
 
     @Override
-    public T fromEncodedString(final Format format, final String encodedData) {
-        return valueSerializer.fromEncodedString(format, encodedData);
+    public final T destring(final Format format, final String encodedData) {
+        return valueSerializer.destring(format, encodedData);
     }
 
     @Override
-    public String toEncodedString(final Format format, final T value) {
-        return valueSerializer.toEncodedString(format, value);
+    public final String enstring(final Format format, final T value) {
+        return valueSerializer.enstring(format, value);
     }
 
     // -- ORDER RELATION
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializer.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializer.java
index a3ee0d05fc..772e8d3429 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializer.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializer.java
@@ -18,21 +18,26 @@
  */
 package org.apache.isis.core.metamodel.facets.object.value;
 
+import org.springframework.lang.Nullable;
+
+import lombok.NonNull;
+
 public interface ValueSerializer<T> {
 
     public enum Format {
         JSON,
+        URL_SAFE,
         //XML
     }
 
     /**
      * Converts a string of provided {@link Format} to an instance of the object.
      */
-    T fromEncodedString(Format format, String encodedData);
+    T destring(@NonNull Format format, @NonNull String encodedData);
 
     /**
      * Converts the provided object into provided {@link Format}.
      */
-    String toEncodedString(Format format, T value);
+    String enstring(@NonNull Format format, @Nullable T value);
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializerDefault.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializerDefault.java
index 8d07fd0212..94f3c644db 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializerDefault.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/value/ValueSerializerDefault.java
@@ -18,10 +18,13 @@
  */
 package org.apache.isis.core.metamodel.facets.object.value;
 
+import org.springframework.lang.Nullable;
+
 import org.apache.isis.applib.value.semantics.ValueDecomposition;
 import org.apache.isis.applib.value.semantics.ValueSemanticsProvider;
-import org.apache.isis.commons.internal.assertions._Assert;
 import org.apache.isis.commons.internal.base._Casts;
+import org.apache.isis.commons.internal.base._Strings;
+import org.apache.isis.commons.internal.exceptions._Exceptions;
 
 import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
@@ -35,21 +38,34 @@ implements ValueSerializer<T> {
     private final @NonNull ValueSemanticsProvider<T> semantics;
 
     @Override
-    public T fromEncodedString(final Format format, final String encodedData) {
-        _Assert.assertNotNull(encodedData);
+    public T destring(final @NonNull Format format, final @NonNull String encodedData) {
         if (ENCODED_NULL.equals(encodedData)) {
             return null;
-        } else {
+        }
+        switch(format) {
+        case JSON:
             return semantics.compose(
                     ValueDecomposition.fromJson(semantics.getSchemaValueType(), encodedData));
+        case URL_SAFE:
+            //TODO could use IdStringifiers instead
+            return destring(Format.JSON, _Strings.base64UrlDecode(encodedData));
         }
+        throw _Exceptions.unmatchedCase(format);
     }
 
     @Override
-    public String toEncodedString(final Format format, final T value) {
-        return value == null
-                ? ENCODED_NULL
-                : semantics.decompose(_Casts.uncheckedCast(value)).toJson();
+    public String enstring(final @NonNull Format format, final @Nullable T value) {
+        if(value == null) {
+            return ENCODED_NULL;
+        }
+        switch(format) {
+        case JSON:
+            return semantics.decompose(_Casts.uncheckedCast(value)).toJson();
+        case URL_SAFE:
+            //TODO could use IdStringifiers instead
+            return _Strings.base64UrlEncode(enstring(Format.JSON, value));
+        }
+        throw _Exceptions.unmatchedCase(format);
     }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
index 8c3c1d8328..32c153b06e 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/facets/object/viewmodel/ViewModelFacetAbstract.java
@@ -101,9 +101,7 @@ implements ViewModelFacet {
 
     @Override
     public final Bookmark serializeToBookmark(final @NonNull ManagedObject managedObject) {
-        return Bookmark.forLogicalTypeAndIdentifier(
-                managedObject.getSpecification().getLogicalType(),
-                serialize(managedObject));
+        return managedObject.createBookmark(serialize(managedObject));
     }
 
     protected abstract @NonNull String serialize(@NonNull ManagedObject managedObject);
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObject.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObject.java
index 8997ef491a..ef2646d3af 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObject.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObject.java
@@ -30,6 +30,7 @@ import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.metamodel.context.HasMetaModelContext;
 import org.apache.isis.core.metamodel.facets.object.icon.ObjectIcon;
 import org.apache.isis.core.metamodel.object.ManagedObject.Specialization.BookmarkPolicy;
+import org.apache.isis.core.metamodel.spec.HasObjectSpecification;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
 
@@ -50,7 +51,8 @@ import lombok.extern.log4j.Log4j2;
 public interface ManagedObject
 extends
     Bookmarkable,
-    HasMetaModelContext {
+    HasMetaModelContext,
+    HasObjectSpecification {
 
     /**
      * ManagedObject specializations have varying contract/behavior.
@@ -316,6 +318,7 @@ extends
     /**
      * Returns the specification that details the structure (meta-model) of this object.
      */
+    @Override
     ObjectSpecification getSpecification();
 
     /**
@@ -351,15 +354,6 @@ extends
      */
     String getTitle();
 
-    // -- SHORTCUT - ELEMENT SPECIFICATION
-
-    /**
-     * As used for the element type of collections.
-     */
-    default Optional<ObjectSpecification> getElementSpecification() {
-        return getSpecification().getElementSpecification();
-    }
-
     // -- SHORTCUT - ICON
 
     /**
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObjects.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObjects.java
index ee6f7b2be3..f642afe7db 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObjects.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/ManagedObjects.java
@@ -22,6 +22,7 @@ import java.lang.reflect.Method;
 import java.util.Comparator;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.UUID;
 import java.util.function.Supplier;
 
 import org.springframework.lang.Nullable;
@@ -161,6 +162,14 @@ public final class ManagedObjects {
                 .orElseThrow(()->_Exceptions.illegalArgument("cannot identify %s", managedObject));
     }
 
+    /**
+     * eg. transient entities have no bookmark, so can fallback to UUID
+     */
+    public static Bookmark bookmarkElseUUID(final @Nullable ManagedObject managedObject) {
+        return bookmark(managedObject)
+                .orElseGet(()->managedObject.createBookmark(UUID.randomUUID().toString()));
+    }
+
     /**
      * @param managedObject
      * @return optionally a String representing a reference to the <em>identifiable</em>
@@ -368,7 +377,7 @@ public final class ManagedObjects {
         .collect(Can.toCan());
     }
 
-    
+
 
     // -- IMPERATIVE TEXT UTILITY
 
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectService.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectService.java
index fd1f168a9f..57e409f01c 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectService.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectService.java
@@ -80,9 +80,7 @@ extends _ManagedObjectSpecified {
     // -- HELPER
 
     private Bookmark createBookmark() {
-        return Bookmark.forLogicalTypeAndIdentifier(
-                getSpecification().getLogicalType(),
-                "1");
+        return createBookmark("1");
     }
 
 }
\ No newline at end of file
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectValue.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectValue.java
index f6795095af..4bcabc9681 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectValue.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/object/_ManagedObjectValue.java
@@ -77,15 +77,12 @@ extends _ManagedObjectSpecified {
     // -- HELPER
 
     private ValueFacet<?> valueFacet() {
-        return getSpecification().valueFacet().orElseThrow();
+        return getSpecification().valueFacetElseFail();
     }
 
     private Bookmark createBookmark() {
-        //TODO if value semantics providers are enforced to provide an IdStringifier,
-        // we could use that instead (to generate the second argument)!
-        return Bookmark.forLogicalTypeAndIdentifier(
-                getSpecification().getLogicalType(),
-                valueFacet().toEncodedString(Format.JSON, _Casts.uncheckedCast(getPojo())));
+        return createBookmark(
+                valueFacet().enstring(Format.URL_SAFE, _Casts.uncheckedCast(getPojo())));
     }
 
 }
\ No newline at end of file
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
index 165b141cfa..bf1229edf1 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/ObjectManager.java
@@ -142,7 +142,7 @@ public interface ObjectManager {
             return ManagedObject.unspecified();
         }
         return spec.isScalar()
-                ? managedObjectEagerlyBookmarkedIfRequired(spec, pojo)
+                ? ManagedObject.adaptScalar(spec, pojo)
                 : ManagedObject.packed(
                         spec.getElementSpecification().orElseGet(fallbackElementType),
                         _NullSafe.streamAutodetect(pojo)
@@ -173,25 +173,11 @@ public interface ObjectManager {
                 || pojo.getClass().equals(proposedSpec.getCorrespondingClass()))
             // if actual type matches spec's, we assume, that we don't need to reload,
             // so this is a shortcut for performance reasons
-            ? managedObjectEagerlyBookmarkedIfRequired(
-                    proposedSpec, pojo)
+            ? ManagedObject.adaptScalar(proposedSpec, pojo)
             // fallback, ignoring proposedSpec
             : adapt(pojo);
         return adapter;
     }
 
-    // -- HELPER
-
-    /**
-     * {@link ManagedObject} factory, that in case of given pojo representing an entity
-     * and the entityAdaptingMode equals {@link EntityAdaptingMode#isBookmarkable()},
-     * then tries to memoize its {@link Bookmark} eagerly
-     * (otherwise its {@link Bookmark} is lazily resolved).
-     */
-    private static ManagedObject managedObjectEagerlyBookmarkedIfRequired(
-            final ObjectSpecification spec,
-            final Object pojo) {
-        return ManagedObject.adaptScalar(spec, pojo);
-    }
 
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker.java
index 1a527d69b0..a3ca1217da 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker.java
@@ -42,6 +42,7 @@ public interface ObjectBookmarker {
                     "ObjectBookmarker",
                     _Lists.of(
                             new ObjectBookmarker_builtinHandlers.GuardAgainstOid(),
+                            new ObjectBookmarker_builtinHandlers.BookmarkForNonScalar(),
                             new ObjectBookmarker_builtinHandlers.BookmarkForServices(),
                             new ObjectBookmarker_builtinHandlers.BookmarkForValues(),
                             new ObjectBookmarker_builtinHandlers.BookmarkForViewModels(),
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker_builtinHandlers.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker_builtinHandlers.java
index 9130d1b88d..41a2579403 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker_builtinHandlers.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/objectmanager/identify/ObjectBookmarker_builtinHandlers.java
@@ -18,16 +18,11 @@
  */
 package org.apache.isis.core.metamodel.objectmanager.identify;
 
-import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 
 import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.bookmark.Oid;
-import org.apache.isis.applib.value.semantics.ValueSemanticsProvider;
-import org.apache.isis.commons.internal.base._Bytes;
-import org.apache.isis.commons.internal.base._Strings;
-import org.apache.isis.commons.internal.exceptions._Exceptions;
-import org.apache.isis.core.metamodel.facets.object.viewmodel.ViewModelFacet;
+import org.apache.isis.commons.internal.assertions._Assert;
 import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.object.PackedManagedObject;
 import org.apache.isis.core.metamodel.objectmanager.identify.ObjectBookmarker.Handler;
@@ -37,8 +32,6 @@ import lombok.val;
 
 class ObjectBookmarker_builtinHandlers {
 
-    public static final String SERVICE_IDENTIFIER = "1";
-
     static class GuardAgainstOid implements Handler {
 
         @Override
@@ -55,6 +48,20 @@ class ObjectBookmarker_builtinHandlers {
 
     }
 
+    static class BookmarkForNonScalar implements Handler {
+
+        @Override
+        public boolean isHandling(final ManagedObject managedObject) {
+            return managedObject instanceof PackedManagedObject;
+        }
+
+        @Override
+        public Bookmark handle(final ManagedObject managedObject) {
+            return bookmarkWithRandomUUID(managedObject);
+        }
+
+    }
+
     static class BookmarkForServices implements Handler {
 
         @Override
@@ -77,7 +84,8 @@ class ObjectBookmarker_builtinHandlers {
 
         @Override
         public Bookmark handle(final ManagedObject managedObject) {
-            return managedObject.getBookmark().orElseThrow();
+            return managedObject.getBookmark()
+                    .orElseGet(()->bookmarkWithRandomUUID(managedObject)); // transient
         }
 
     }
@@ -86,33 +94,14 @@ class ObjectBookmarker_builtinHandlers {
 
         @Override
         public boolean isHandling(final ManagedObject managedObject) {
-            return managedObject.getSpecification().isValue();
+            return managedObject.getSpecialization().isValue();
         }
 
         @SneakyThrows
         @Override
         public Bookmark handle(final ManagedObject managedObject) {
-            val spec = managedObject.getSpecification();
-            val valuePojo = managedObject.getPojo();
-            if(valuePojo==null) {
-                return Bookmark.forLogicalTypeAndIdentifier(spec.getLogicalType(), "{}");
-            }
-
-            val valueFacet = spec.valueFacet().orElse(null);
-            ValueSemanticsProvider<Object> composer = (ValueSemanticsProvider) valueFacet.selectDefaultSemantics()
-                    .orElseThrow(()->_Exceptions.illegalArgument(
-                            "Cannot create a bookmark for the value type %s, "
-                          + "as no appropriate ValueSemanticsProvider could be found.",
-                          managedObject.getSpecification().getCorrespondingClass().getName()));
-
-            val valueAsJson = composer.decompose(managedObject.getPojo())
-                    .toJson();
-
-            val identifier = _Strings.ofBytes(
-                    _Bytes.asUrlBase64.apply(valueAsJson.getBytes()),
-                    StandardCharsets.UTF_8);
-
-            return Bookmark.forLogicalTypeAndIdentifier(spec.getLogicalType(), identifier);
+            _Assert.assertTrue(managedObject.isBookmarkSupported(), ()->"is bookmarkable");
+            return managedObject.getBookmark().orElseThrow();
         }
 
     }
@@ -121,9 +110,7 @@ class ObjectBookmarker_builtinHandlers {
 
         @Override
         public boolean isHandling(final ManagedObject managedObject) {
-            return (managedObject instanceof PackedManagedObject)
-                    ? false
-                    : managedObject.getSpecification().containsFacet(ViewModelFacet.class);
+            return managedObject.getSpecialization().isViewmodel();
         }
 
         @Override
@@ -134,8 +121,7 @@ class ObjectBookmarker_builtinHandlers {
             }
 
             val spec = managedObject.getSpecification();
-            val recreatableObjectFacet = spec.getFacet(ViewModelFacet.class);
-            return recreatableObjectFacet.serializeToBookmark(managedObject);
+            return spec.viewmodelFacetElseFail().serializeToBookmark(managedObject);
         }
 
     }
@@ -149,10 +135,16 @@ class ObjectBookmarker_builtinHandlers {
 
         @Override
         public Bookmark handle(final ManagedObject managedObject) {
-            val spec = managedObject.getSpecification();
-            val identifier = UUID.randomUUID().toString();
-            return Bookmark.forLogicalTypeAndIdentifier(spec.getLogicalType(), identifier);
+            return bookmarkWithRandomUUID(managedObject);
         }
     }
 
+    // -- HELPER
+
+    private static Bookmark bookmarkWithRandomUUID(final ManagedObject managedObject) {
+        val uuid = UUID.randomUUID().toString();
+        System.err.printf("called bookmarkWithRandomUUID %s [%s]%n", managedObject.getSpecification(), uuid);
+        return managedObject.createBookmark(UUID.randomUUID().toString());
+    }
+
 }
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/HasObjectSpecification.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/HasObjectSpecification.java
new file mode 100644
index 0000000000..c1c33fab84
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/spec/HasObjectSpecification.java
@@ -0,0 +1,60 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.apache.isis.core.metamodel.spec;
+
+import java.util.Optional;
+
+import org.apache.isis.applib.id.LogicalType;
+import org.apache.isis.applib.services.bookmark.Bookmark;
+
+import lombok.NonNull;
+
+/**
+ * Introduced as a shortcut provider.
+ */
+public interface HasObjectSpecification {
+
+    ObjectSpecification getSpecification();
+
+    // -- SHORTCUTS
+
+    default Class<?> getCorrespondingClass() {
+        return getSpecification().getCorrespondingClass();
+    }
+
+    default LogicalType getLogicalType() {
+        return getSpecification().getLogicalType();
+    }
+
+    default String getLogicalTypeName() {
+        return getSpecification().getLogicalTypeName();
+    }
+
+    /**
+     * As used for the element type of collections.
+     */
+    default Optional<ObjectSpecification> getElementSpecification() {
+        return getSpecification().getElementSpecification();
+    }
+
+    default Bookmark createBookmark(final @NonNull String urlSafeIdentifier) {
+        return Bookmark.forLogicalTypeAndIdentifier(getLogicalType(), urlSafeIdentifier);
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/snapshot/XmlSnapshot.java b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/snapshot/XmlSnapshot.java
index 93a2ff4cb9..15f5876e44 100644
--- a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/snapshot/XmlSnapshot.java
+++ b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/snapshot/XmlSnapshot.java
@@ -41,6 +41,7 @@ import org.apache.isis.applib.ViewModel;
 import org.apache.isis.applib.exceptions.UnrecoverableException;
 import org.apache.isis.applib.services.xmlsnapshot.XmlSnapshotService.Snapshot;
 import org.apache.isis.applib.snapshot.SnapshottableWithInclusions;
+import org.apache.isis.commons.internal.base._Strings;
 import org.apache.isis.commons.internal.codec._DocumentFactories;
 import org.apache.isis.commons.internal.collections._Maps;
 import org.apache.isis.core.metamodel.consent.InteractionInitiatedBy;
@@ -682,12 +683,12 @@ public class XmlSnapshot implements Snapshot {
                     log.debug("objectToElement(NO): {} is value", log("field", fieldName));
                 }
 
-                final ObjectSpecification fieldNos = field.getElementType();
+                final ObjectSpecification fieldSpec = field.getElementType();
                 // skip fields of type XmlValue
-                if (fieldNos == null) {
+                if (fieldSpec == null) {
                     continue eachField;
                 }
-                if (fieldNos.getFullIdentifier() != null && fieldNos.getFullIdentifier().endsWith("XmlValue")) {
+                if (fieldSpec.getFullIdentifier() != null && fieldSpec.getFullIdentifier().endsWith("XmlValue")) {
                     continue eachField;
                 }
 
@@ -701,22 +702,17 @@ public class XmlSnapshot implements Snapshot {
                 try {
                     value = valueAssociation.get(adapter, InteractionInitiatedBy.FRAMEWORK);
 
-                    final ObjectSpecification valueNos = value.getSpecification();
+                    val valueSpec = value.getSpecification();
 
                     // XML
-                    isisMetaModel.setAttributesForValue(xmlValueElement, valueNos.getShortIdentifier());
+                    isisMetaModel.setAttributesForValue(xmlValueElement, valueSpec.getShortIdentifier());
 
-                    // return encoded string, else title.
-                    String valueStr;
-                    val valueFacet = fieldNos.valueFacet().orElse(null);
-                    if (valueFacet != null) {
-                        valueStr = valueFacet.toEncodedString(Format.JSON, value.getPojo());
-                    } else {
-                        valueStr = value.getTitle();
-                    }
+                    // value as JSON
+                    @SuppressWarnings("unchecked")
+                    val valueStr = fieldSpec.valueFacetElseFail()
+                            .enstring(Format.JSON, value.getPojo());
 
-                    final boolean notEmpty = (valueStr.length() > 0);
-                    if (notEmpty) {
+                    if (_Strings.isNotEmpty(valueStr)) {
                         xmlValueElement.appendChild(getXmlDocument().createTextNode(valueStr));
                     } else {
                         isisMetaModel.setIsEmptyAttribute(xmlValueElement, true);
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaTimeValueSemanticsProviderTest.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaTimeValueSemanticsProviderTest.java
index 694c732de7..bff7dfaa3b 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaTimeValueSemanticsProviderTest.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/JavaTimeValueSemanticsProviderTest.java
@@ -25,6 +25,9 @@ import java.util.Locale;
 
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
 import org.apache.isis.applib.annotation.TimePrecision;
 import org.apache.isis.applib.exceptions.recoverable.TextEntryParseException;
 import org.apache.isis.applib.locale.UserLocale;
@@ -35,9 +38,6 @@ import org.apache.isis.applib.value.semantics.ValueSemanticsProvider.Context;
 import org.apache.isis.core.metamodel.valuesemantics.temporal.LocalDateTimeValueSemantics;
 import org.apache.isis.core.metamodel.valuesemantics.temporal.legacy.JavaUtilDateValueSemantics;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
 import lombok.NonNull;
 import lombok.val;
 
@@ -146,11 +146,6 @@ extends ValueSemanticsProviderAbstractTestCase<java.util.Date> {
         return date;
     }
 
-    @Override
-    public void testValueSerializer_usingJson() {
-        // TODO fails with NPE
-    }
-
     @Override
     protected void assertValueEncodesToJsonAs(final Date a, final String json) {
         // TODO
diff --git a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ValueSemanticsProviderAbstractTestCase.java b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ValueSemanticsProviderAbstractTestCase.java
index 04d5323baa..71581794b1 100644
--- a/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ValueSemanticsProviderAbstractTestCase.java
+++ b/core/metamodel/src/test/java/org/apache/isis/core/metamodel/facets/value/ValueSemanticsProviderAbstractTestCase.java
@@ -29,6 +29,14 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import org.apache.isis.applib.services.iactn.InteractionProvider;
 import org.apache.isis.applib.value.semantics.Parser;
@@ -41,16 +49,10 @@ import org.apache.isis.core.metamodel._testing.MetaModelContext_forTesting;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
 import org.apache.isis.core.metamodel.facets.object.value.ValueSerializer;
 import org.apache.isis.core.metamodel.facets.object.value.ValueSerializer.Format;
-import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.facets.object.value.ValueSerializerDefault;
+import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.valuesemantics.StringValueSemantics;
 
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
 import lombok.Getter;
 
 public abstract class ValueSemanticsProviderAbstractTestCase<T> {
@@ -135,14 +137,17 @@ public abstract class ValueSemanticsProviderAbstractTestCase<T> {
 
     }
 
-    @Test
-    public void testValueSerializer_usingJson() {
+    @ParameterizedTest
+    @EnumSource(Format.class)
+    public void testValueSerializer(final Format format) {
+        assumeValueSemanticsProviderIsSetup();
+
         final T value = getSample();
-        final String encoded = getValueSerializer().toEncodedString(Format.JSON, value);
+        final String encoded = getValueSerializer().enstring(format, value);
 
         assertValueEncodesToJsonAs(value, encoded);
 
-        T decoded = getValueSerializer().fromEncodedString(Format.JSON, encoded);
+        T decoded = getValueSerializer().destring(format, encoded);
 
         Optional.ofNullable(semantics.getOrderRelation())
             .ifPresentOrElse(rel->Assertions.assertTrue(rel.equals(value, decoded)),
@@ -153,21 +158,23 @@ public abstract class ValueSemanticsProviderAbstractTestCase<T> {
     protected abstract void assertValueEncodesToJsonAs(T a, String json);
 
 
-    @Test
-    public void testDecodeNULL() throws Exception {
+    @ParameterizedTest
+    @EnumSource(Format.class)
+    public void testDecodeNULL(final Format format) throws Exception {
         assumeValueSemanticsProviderIsSetup();
 
         final Object newValue = getValueSerializer()
-                .fromEncodedString(Format.JSON, ValueSerializerDefault.ENCODED_NULL);
+                .destring(format, ValueSerializerDefault.ENCODED_NULL);
         assertNull(newValue);
     }
 
-    @Test
-    public void testEmptyEncoding() {
+    @ParameterizedTest
+    @EnumSource(Format.class)
+    public void testEmptyEncoding(final Format format) {
         assumeValueSemanticsProviderIsSetup();
 
         assertEquals(ValueSerializerDefault.ENCODED_NULL, getValueSerializer()
-                .toEncodedString(Format.JSON, null));
+                .enstring(format, null));
     }
 
     @Test
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
index dd5a2de831..063ea58bd9 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/IsisModuleCoreRuntime.java
@@ -25,7 +25,7 @@ import org.apache.isis.core.interaction.IsisModuleCoreInteraction;
 import org.apache.isis.core.metamodel.IsisModuleCoreMetamodel;
 import org.apache.isis.core.runtime.events.MetamodelEventService;
 import org.apache.isis.core.runtime.events.TransactionEventEmitter;
-import org.apache.isis.core.runtime.idstringifier.IdStringifierService;
+import org.apache.isis.core.runtime.idstringifier.IdStringifierLookupService;
 import org.apache.isis.core.transaction.IsisModuleCoreTransaction;
 import org.apache.isis.valuetypes.jodatime.integration.IsisModuleValJodatimeIntegration;
 
@@ -42,7 +42,7 @@ import org.apache.isis.valuetypes.jodatime.integration.IsisModuleValJodatimeInte
         // @Service's
         MetamodelEventService.class,
         TransactionEventEmitter.class,
-        IdStringifierService.class,
+        IdStringifierLookupService.class,
 
         // @Configuration's
 
diff --git a/core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierService.java b/core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierLookupService.java
similarity index 68%
rename from core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierService.java
rename to core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierLookupService.java
index dcece158bf..b5101a87e6 100644
--- a/core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierService.java
+++ b/core/runtime/src/main/java/org/apache/isis/core/runtime/idstringifier/IdStringifierLookupService.java
@@ -35,14 +35,13 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.ClassUtils;
 
 import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.annotation.ValueSemantics;
 import org.apache.isis.applib.services.bookmark.IdStringifier;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Casts;
 import org.apache.isis.commons.internal.exceptions._Exceptions;
+import org.apache.isis.core.metamodel.facets.object.entity.EntityFacet.PrimaryKeyType;
 import org.apache.isis.core.runtime.IsisModuleCoreRuntime;
 
-import lombok.NonNull;
 import lombok.val;
 
 /**
@@ -51,24 +50,19 @@ import lombok.val;
  * <p>
  * This is intended for framework use, there is little reason to call it or override it.
  *
- * @implNote yet does not support per member ValueSemantics selection;
- *      future work would look for {@link ValueSemantics} annotations on primary key members and
- *      would then honor {@link ValueSemantics#provider()} attribute,
- *      to narrow the {@link IdStringifier} search
- *
  * @since 2.0
  */
 @Service
-@Named(IsisModuleCoreRuntime.NAMESPACE + ".IdStringifierService")
+@Named(IsisModuleCoreRuntime.NAMESPACE + ".IdStringifierLookupService")
 @Priority(PriorityPrecedence.MIDPOINT)
 @Qualifier("Default")
-public class IdStringifierService {
+public class IdStringifierLookupService {
 
     private final Can<IdStringifier<?>> idStringifiers;
     private final Map<Class<?>, IdStringifier<?>> stringifierByClass = new ConcurrentHashMap<>();
 
     @Inject
-    public IdStringifierService(
+    public IdStringifierLookupService(
             final List<IdStringifier<?>> idStringifiers,
             final Optional<IdStringifier<Serializable>> idStringifierForSerializableIfAny) {
         // IdStringifierForSerializable is enforced to go last, so any custom IdStringifier(s)
@@ -82,31 +76,23 @@ public class IdStringifierService {
         this.idStringifiers = Can.ofCollection(idStringifiers);
     }
 
-    public <T> String enstringPrimaryKey(final @NonNull Class<T> primaryKeyType, final @NonNull Object primaryKey) {
-        val idStringifier = lookupElseFail(ClassUtils.resolvePrimitiveIfNecessary(primaryKeyType));
-        return idStringifier.enstring(_Casts.uncheckedCast(primaryKey));
-    }
-
-    public <T> T destringPrimaryKey(
-            final @NonNull Class<T> primaryKeyType,
-            final @NonNull Class<?> entityClass,
-            final @NonNull String stringifiedId) {
-        val idStringifier = lookupElseFail(ClassUtils.resolvePrimitiveIfNecessary(primaryKeyType));
-        val primaryKey = idStringifier.destring(entityClass, stringifiedId);
-        return _Casts.uncheckedCast(primaryKey);
+    public <T> PrimaryKeyType<T> primaryKeyTypeFor(
+            final Class<?> entityClass, final Class<T> primaryKeyType) {
+        return PrimaryKeyType.getInstance(entityClass,
+                this::lookupIdStringifierElseFail,
+                primaryKeyType);
     }
 
-    // -- HELPER
-
-    private <T> IdStringifier<T> lookupElseFail(final Class<T> candidateValueClass) {
-        return lookup(candidateValueClass)
+    public <T> IdStringifier<T> lookupIdStringifierElseFail(final Class<T> candidateValueClass) {
+        return lookupIdStringifier(candidateValueClass)
             .orElseThrow(() -> _Exceptions.noSuchElement(
                     "Could not locate an IdStringifier to handle '%s'",
                     candidateValueClass));
     }
 
-    private <T> Optional<IdStringifier<T>> lookup(final Class<T> candidateValueClass) {
-        val idStringifier = stringifierByClass.computeIfAbsent(candidateValueClass, aClass -> {
+    public <T> Optional<IdStringifier<T>> lookupIdStringifier(final Class<T> candidateValueClass) {
+        val idStringifier = stringifierByClass.computeIfAbsent(
+                ClassUtils.resolvePrimitiveIfNecessary(candidateValueClass), aClass -> {
             for (val candidateStringifier : idStringifiers) {
                 if (candidateStringifier.getCorrespondingClass().isAssignableFrom(candidateValueClass)) {
                     return candidateStringifier;
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandDtoFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandDtoFactoryDefault.java
index 2112294f9c..a11ae430f2 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandDtoFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/command/CommandDtoFactoryDefault.java
@@ -169,7 +169,8 @@ public class CommandDtoFactoryDefault implements CommandDtoFactory {
         dto.setUsername(userService.currentUserNameElseNobody());
         dto.setTimestamp(clockService.getClock().nowAsXmlGregorianCalendar());
 
-        final Bookmark bookmark = ManagedObjects.bookmarkElseFail(targetHead.getOwner());
+        // transient entities have no bookmark, so fallback to UUID
+        final Bookmark bookmark = ManagedObjects.bookmarkElseUUID(targetHead.getOwner());
         final OidsDto targetOids = CommandDtoUtils.targetsFor(dto);
         targetOids.getOid().add(bookmark.toOidDto());
 
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/interaction/InteractionDtoFactoryDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/interaction/InteractionDtoFactoryDefault.java
index a3201a6aac..96816d0ea5 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/interaction/InteractionDtoFactoryDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/interaction/InteractionDtoFactoryDefault.java
@@ -28,7 +28,6 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
 import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.applib.services.iactn.Interaction;
 import org.apache.isis.applib.services.iactn.InteractionProvider;
 import org.apache.isis.applib.services.user.UserService;
@@ -36,7 +35,6 @@ import org.apache.isis.applib.util.schema.CommandDtoUtils;
 import org.apache.isis.applib.util.schema.InteractionDtoUtils;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.assertions._Assert;
-import org.apache.isis.commons.internal.exceptions._Exceptions;
 import org.apache.isis.core.metamodel.execution.InteractionInternal;
 import org.apache.isis.core.metamodel.interactions.InteractionHead;
 import org.apache.isis.core.metamodel.object.ManagedObject;
@@ -86,8 +84,10 @@ public class InteractionDtoFactoryDefault implements InteractionDtoFactory {
         final int nextEventSequence = ((InteractionInternal) interaction).getThenIncrementExecutionSequence();
 
         val owner = head.getOwner();
-        final Bookmark targetBookmark = owner.getBookmark()
-                .orElseThrow(()->_Exceptions.noSuchElement("Object provides no Bookmark: %s", owner));
+
+        // transient entities have no bookmark, so fallback to UUID
+        val targetBookmark = ManagedObjects.bookmarkElseUUID(owner);
+                //.orElseThrow(()->_Exceptions.noSuchElement("Object provides no Bookmark: %s", owner));
 
         final String currentUser = userService.currentUserNameElseNobody();
 
@@ -132,8 +132,9 @@ public class InteractionDtoFactoryDefault implements InteractionDtoFactory {
         final Interaction interaction = interactionProviderProvider.get().currentInteractionElseFail();
         final int nextEventSequence = ((InteractionInternal) interaction).getThenIncrementExecutionSequence();
 
-        final Bookmark targetBookmark = targetAdapter.getBookmark()
-                .orElseThrow(()->_Exceptions.noSuchElement("Object provides no Bookmark: %s", targetAdapter));
+        // transient entities have no bookmark, so fallback to UUID
+        val targetBookmark = ManagedObjects.bookmarkElseUUID(targetAdapter);
+                //.orElseThrow(()->_Exceptions.noSuchElement("Object provides no Bookmark: %s", owner));
 
         final String currentUser = userService.currentUserNameElseNobody();
 
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/ObjectMementoServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/ObjectMementoServiceDefault.java
index d398c0e8ed..cc7edb7368 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/ObjectMementoServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/ObjectMementoServiceDefault.java
@@ -82,7 +82,7 @@ public class ObjectMementoServiceDefault implements ObjectMementoService {
         if(mementoAdapter==null) {
             // sonar-ignore-on (fails to detect this as null guard)
             return ManagedObjects.isSpecified(adapter)
-                    ? new ObjectMementoForEmpty(adapter.getSpecification().getLogicalType())
+                    ? new ObjectMementoForEmpty(adapter.getLogicalType())
                     : null;
             // sonar-ignore-on
         }
@@ -96,7 +96,7 @@ public class ObjectMementoServiceDefault implements ObjectMementoService {
                 .collect(Collectors.toCollection(ArrayList::new)); // ArrayList is serializable
         return ObjectMementoCollection.of(
                 listOfMementos,
-                packedAdapter.getSpecification().getLogicalType());
+                packedAdapter.getLogicalType());
     }
 
     @Override
@@ -106,7 +106,7 @@ public class ObjectMementoServiceDefault implements ObjectMementoService {
         }
         val mementoAdapter = _ObjectMemento.createOrNull(paramAdapter);
         if(mementoAdapter==null) {
-            return new ObjectMementoForEmpty(paramAdapter.getSpecification().getLogicalType());
+            return new ObjectMementoForEmpty(paramAdapter.getLogicalType());
         }
         return ObjectMementoAdapter.of(mementoAdapter);
     }
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/_ObjectMemento.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/_ObjectMemento.java
index 4cf1ed7816..80714a4f9b 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/_ObjectMemento.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/memento/_ObjectMemento.java
@@ -22,7 +22,6 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Objects;
-import java.util.function.Function;
 
 import org.springframework.lang.Nullable;
 
@@ -45,9 +44,7 @@ import org.apache.isis.core.metamodel.spec.ObjectSpecification;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
 import org.apache.isis.core.metamodel.util.Facets;
 
-import lombok.AccessLevel;
 import lombok.Getter;
-import lombok.NoArgsConstructor;
 import lombok.NonNull;
 import lombok.val;
 import lombok.extern.log4j.Log4j2;
@@ -112,55 +109,56 @@ final class _ObjectMemento implements HasLogicalType, Serializable {
                 return memento.recreateStrategy.equals(memento, otherMemento);
             }
         },
-        /**
-         * represents a list of objects
-         */
-        VECTOR {
-
-            @Override
-            public ManagedObject asAdapter(
-                    final _ObjectMemento memento,
-                    final MetaModelContext mmc) {
-
-                // I believe this code path is no longer reachable
-                throw _Exceptions.unexpectedCodeReach();
-
-//                final Can<ManagedObject> managedObjects =
-//                        _NullSafe.stream(memento.list)
-//                        .map(Functions.toManagedObject(mmc))
-//                        .collect(Can.toCan());
+//        /**
+//         * represents a list of objects
+//         */
+//        VECTOR {
 //
-//                val commonSpec = ManagedObjects.commonSpecification(managedObjects)
-//                        .orElseGet(()->mmc.getSpecificationLoader().loadSpecification(Object.class));
+//            @Override
+//            public ManagedObject asAdapter(
+//                    final _ObjectMemento memento,
+//                    final MetaModelContext mmc) {
 //
-//                return ManagedObject.packed(commonSpec, managedObjects);
-            }
-
-            @Override
-            public Bookmark asPseudoBookmark(final _ObjectMemento memento) {
-                return Bookmark.forLogicalTypeNameAndIdentifier(
-                        memento.getLogicalTypeName(),
-                        memento.list.toString());
-            }
-
-            @Override
-            public int hashCode(final _ObjectMemento memento) {
-                return memento.list.hashCode();
-            }
-
-            @Override
-            public boolean equals(final _ObjectMemento memento, final Object other) {
-                if (!(other instanceof _ObjectMemento)) {
-                    return false;
-                }
-                final _ObjectMemento otherMemento = (_ObjectMemento) other;
-                if(otherMemento.cardinality != VECTOR) {
-                    return false;
-                }
-                return memento.list.equals(otherMemento.list);
-            }
-
-        };
+//                // I believe this code path is no longer reachable
+//                throw _Exceptions.unexpectedCodeReach();
+//
+////                final Can<ManagedObject> managedObjects =
+////                        _NullSafe.stream(memento.list)
+////                        .map(Functions.toManagedObject(mmc))
+////                        .collect(Can.toCan());
+////
+////                val commonSpec = ManagedObjects.commonSpecification(managedObjects)
+////                        .orElseGet(()->mmc.getSpecificationLoader().loadSpecification(Object.class));
+////
+////                return ManagedObject.packed(commonSpec, managedObjects);
+//            }
+//
+//            @Override
+//            public Bookmark asPseudoBookmark(final _ObjectMemento memento) {
+//                return Bookmark.forLogicalTypeNameAndIdentifier(
+//                        memento.getLogicalTypeName(),
+//                        memento.list.toString());
+//            }
+//
+//            @Override
+//            public int hashCode(final _ObjectMemento memento) {
+//                return memento.list.hashCode();
+//            }
+//
+//            @Override
+//            public boolean equals(final _ObjectMemento memento, final Object other) {
+//                if (!(other instanceof _ObjectMemento)) {
+//                    return false;
+//                }
+//                final _ObjectMemento otherMemento = (_ObjectMemento) other;
+//                if(otherMemento.cardinality != VECTOR) {
+//                    return false;
+//                }
+//                return memento.list.equals(otherMemento.list);
+//            }
+//
+//        }
+        ;
 
         void ensure(final Cardinality sort) {
             if(this == sort) {
@@ -192,14 +190,17 @@ final class _ObjectMemento implements HasLogicalType, Serializable {
                     final _ObjectMemento memento,
                     final MetaModelContext mmc) {
 
-                val valueSerializer = mmc.getSpecificationLoader()
+                val valueSpec = mmc.getSpecificationLoader()
                         .specForLogicalType(memento.logicalType)
-                        .flatMap(spec->Facets.valueSerializer(spec, spec.getCorrespondingClass()))
                         .orElseThrow(()->_Exceptions.unrecoverable(
-                                "logical type %s is expected to have a ValueFacet", memento.logicalType));
+                                "logical type %s is not recognized", memento.logicalType));
 
-                return mmc.getObjectManager().adapt(
-                        valueSerializer.fromEncodedString(Format.JSON, memento.encodableValue));
+                val valueSerializer = Facets.valueSerializer(valueSpec, valueSpec.getCorrespondingClass())
+                        .orElseThrow(()->_Exceptions.unrecoverable(
+                                "logical type %s is expected to have a value semantics", memento.logicalType));
+
+                return ManagedObject.value(valueSpec,
+                        valueSerializer.destring(Format.URL_SAFE, memento.encodableValue));
             }
 
             @Override
@@ -413,9 +414,11 @@ final class _ObjectMemento implements HasLogicalType, Serializable {
             final ArrayList<_ObjectMemento> list,
             final LogicalType logicalType) {
 
-        this.cardinality = Cardinality.VECTOR;
-        this.list = list;
-        this.logicalType = logicalType;
+        throw _Exceptions.unexpectedCodeReach();
+
+        //this.cardinality = Cardinality.VECTOR;
+//        this.list = list;
+//        this.logicalType = logicalType;
     }
 
     private _ObjectMemento(final Bookmark bookmark, final SpecificationLoader specificationLoader) {
@@ -482,7 +485,7 @@ final class _ObjectMemento implements HasLogicalType, Serializable {
                 .orElse(null);
         val isEncodable = valueSerializer != null;
         if (isEncodable) {
-            encodableValue = valueSerializer.toEncodedString(Format.JSON, _Casts.uncheckedCast(adapter.getPojo()));
+            encodableValue = valueSerializer.enstring(Format.URL_SAFE, _Casts.uncheckedCast(adapter.getPojo()));
             recreateStrategy = RecreateStrategy.VALUE;
             return;
         }
@@ -578,29 +581,6 @@ final class _ObjectMemento implements HasLogicalType, Serializable {
         return cardinality.equals(this, obj);
     }
 
-    // -- FUNCTIONS
-
-    @NoArgsConstructor(access = AccessLevel.PRIVATE)
-    private static final class Functions {
-
-        private static Function<_ObjectMemento, ManagedObject> toManagedObject(
-                final MetaModelContext mmc) {
-
-            return memento->{
-                if(memento == null) {
-                    return ManagedObject.unspecified();
-                }
-                val objectAdapter = memento
-                        .reconstructObject(mmc);
-                if(objectAdapter == null) {
-                    return ManagedObject.unspecified();
-                }
-                return objectAdapter;
-            };
-        }
-
-    }
-
     private void ensureScalar() {
         getCardinality().ensure(Cardinality.SCALAR);
     }
diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/entities/DnEntityStateProvider.java b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/entities/DnEntityStateProvider.java
index 0e2fa29a70..25a86cc6e3 100644
--- a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/entities/DnEntityStateProvider.java
+++ b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/entities/DnEntityStateProvider.java
@@ -67,7 +67,8 @@ public class DnEntityStateProvider implements JdoFacetContext {
             return EntityState.NOT_PERSISTABLE;
         }
 
-        if (pojo!=null && pojo instanceof Persistable) {
+        if (pojo!=null
+                && pojo instanceof Persistable) {
             val persistable = (Persistable) pojo;
             val isDeleted = persistable.dnIsDeleted();
             if(isDeleted) {
@@ -75,8 +76,11 @@ public class DnEntityStateProvider implements JdoFacetContext {
             }
             val isPersistent = persistable.dnIsPersistent();
             if(isPersistent) {
-                return persistable.dnGetObjectId()!=null
-                        ? EntityState.PERSISTABLE_ATTACHED
+                val oid = persistable.dnGetObjectId();
+                return oid!=null
+                        ? persistable.dnGetStateManager().isNew(persistable)
+                                ? EntityState.PERSISTABLE_NEW
+                                : EntityState.PERSISTABLE_ATTACHED
                         : EntityState.PERSISTABLE_NEW;
             }
             return EntityState.PERSISTABLE_DETACHED;
diff --git a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
index 6865d76cd1..b86cbe86f1 100644
--- a/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
+++ b/persistence/jdo/datanucleus/src/main/java/org/apache/isis/persistence/jdo/datanucleus/metamodel/facets/entity/JdoEntityFacet.java
@@ -55,14 +55,17 @@ import org.apache.isis.core.metamodel.facets.object.entity.EntityFacet;
 import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.objectmanager.ObjectManager;
 import org.apache.isis.core.metamodel.services.objectlifecycle.ObjectLifecyclePublisher;
-import org.apache.isis.core.runtime.idstringifier.IdStringifierService;
+import org.apache.isis.core.runtime.idstringifier.IdStringifierLookupService;
 import org.apache.isis.persistence.jdo.datanucleus.entities.DnEntityStateProvider;
 import org.apache.isis.persistence.jdo.metamodel.facets.object.persistencecapable.JdoPersistenceCapableFacetFactory;
 import org.apache.isis.persistence.jdo.provider.entities.JdoFacetContext;
 import org.apache.isis.persistence.jdo.spring.integration.TransactionAwarePersistenceManagerFactoryProxy;
 
+import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.NonNull;
 import lombok.val;
+import lombok.experimental.Accessors;
 import lombok.extern.log4j.Log4j2;
 
 /**
@@ -80,10 +83,18 @@ implements EntityFacet {
     @Inject private ObjectManager objectManager;
     @Inject private ExceptionRecognizerService exceptionRecognizerService;
     @Inject private JdoFacetContext jdoFacetContext;
-    @Inject private IdStringifierService idStringifierService;
+    @Inject private ObjectLifecyclePublisher objectLifecyclePublisher;
+
+    @Getter(value = AccessLevel.PROTECTED) @Accessors(fluent = true)
+    @Inject private IdStringifierLookupService idStringifierLookupService;
 
     private final Class<?> entityClass;
 
+    // lazily looks up the primaryKeyTypeFor (needs a PersistenceManager)
+    @Getter(lazy=true, value = AccessLevel.PROTECTED) @Accessors(fluent = true)
+    private final PrimaryKeyType<?> primaryKeyTypeForDecoding = idStringifierLookupService()
+            .primaryKeyTypeFor(entityClass, primaryKeyTypeFor(entityClass));
+
     public JdoEntityFacet(
             final FacetHolder holder, final Class<?> entityClass) {
         super(EntityFacet.class, holder);
@@ -95,6 +106,19 @@ implements EntityFacet {
         return PersistenceStack.JDO;
     }
 
+    /* OPTIMIZATION
+     * even though the IdStringifierLookupService already holds a lookup map,
+     * for performance reasons,
+     * we define another one local to the context of the associated entity class */
+    private final Map<Class<?>, PrimaryKeyType<?>> primaryKeyTypesForEncoding = new ConcurrentHashMap<>();
+    private final PrimaryKeyType<?> primaryKeyTypeForEncoding(final @NonNull Object oid) {
+        val actualPrimaryKeyClass = oid.getClass();
+        val primaryKeyType = primaryKeyTypesForEncoding.computeIfAbsent(actualPrimaryKeyClass, key->
+            idStringifierLookupService()
+                .primaryKeyTypeFor(entityClass, actualPrimaryKeyClass));
+        return primaryKeyType;
+    }
+
     @Override
     public Optional<String> identifierFor(final Object pojo) {
 
@@ -105,9 +129,10 @@ implements EntityFacet {
         val pm = getPersistenceManager();
         var primaryKeyIfAny = pm.getObjectId(pojo);
 
-        return Optional.ofNullable(primaryKeyIfAny)
+        val idIfAny = Optional.ofNullable(primaryKeyIfAny)
                 .map(primaryKey->
-                    idStringifierService.enstringPrimaryKey(primaryKey.getClass(), primaryKey));
+                    primaryKeyTypeForEncoding(primaryKey).enstringWithCast(primaryKey));
+        return idIfAny;
     }
 
     @Override
@@ -119,8 +144,7 @@ implements EntityFacet {
         try {
 
             val persistenceManager = getPersistenceManager();
-            val primaryKey = idStringifierService
-                    .destringPrimaryKey(primaryKeyTypeFor(entityClass), entityClass, bookmark.getIdentifier());
+            val primaryKey = primaryKeyTypeForDecoding().destring(bookmark.getIdentifier());
 
             val fetchPlan = persistenceManager.getFetchPlan();
             fetchPlan.addGroup(FetchGroup.DEFAULT);
@@ -364,13 +388,11 @@ implements EntityFacet {
 
     private Can<ManagedObject> fetchWithinTransaction(final Supplier<List<?>> fetcher) {
 
-        val objectLifecyclePublisher = getFacetHolder().getServiceRegistry()
-                .lookupServiceElseFail(ObjectLifecyclePublisher.class);
-
         return getTransactionalProcessor().callWithinCurrentTransactionElseCreateNew(
                 ()->_NullSafe.stream(fetcher.get())
                     .map(fetchedObject->adopt(objectLifecyclePublisher, fetchedObject))
                     .collect(Can.toCan()))
+                .ifFailureFail()
                 .getValue().orElseThrow();
     }
 
@@ -393,5 +415,4 @@ implements EntityFacet {
         }
     }
 
-
 }
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacet.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacet.java
index 7c764ec931..1eb57800fa 100644
--- a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacet.java
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacet.java
@@ -21,6 +21,7 @@ package org.apache.isis.persistence.jpa.integration.entity;
 import java.lang.reflect.Method;
 import java.util.Optional;
 
+import javax.inject.Inject;
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceException;
 import javax.persistence.PersistenceUnitUtil;
@@ -34,7 +35,6 @@ import org.apache.isis.applib.query.AllInstancesQuery;
 import org.apache.isis.applib.query.NamedQuery;
 import org.apache.isis.applib.query.Query;
 import org.apache.isis.applib.services.bookmark.Bookmark;
-import org.apache.isis.applib.services.registry.ServiceRegistry;
 import org.apache.isis.applib.services.repository.EntityState;
 import org.apache.isis.commons.collections.Can;
 import org.apache.isis.commons.internal.base._Casts;
@@ -45,7 +45,7 @@ import org.apache.isis.core.metamodel.facetapi.FacetAbstract;
 import org.apache.isis.core.metamodel.facetapi.FacetHolder;
 import org.apache.isis.core.metamodel.facets.object.entity.EntityFacet;
 import org.apache.isis.core.metamodel.object.ManagedObject;
-import org.apache.isis.core.runtime.idstringifier.IdStringifierService;
+import org.apache.isis.core.runtime.idstringifier.IdStringifierLookupService;
 
 import lombok.NonNull;
 import lombok.val;
@@ -56,19 +56,22 @@ public class JpaEntityFacet
         extends FacetAbstract
         implements EntityFacet {
 
+    // self managed injections via constructor
+    @Inject private JpaContext jpaContext;
+    @Inject private IdStringifierLookupService idStringifierLookupService;
+
     private final Class<?> entityClass;
-    private final ServiceRegistry serviceRegistry;
-    private final IdStringifierService idStringifierService;
+    private PrimaryKeyType<?> primaryKeyType;
 
     protected JpaEntityFacet(
             final FacetHolder holder,
-            final Class<?> entityClass,
-            final @NonNull ServiceRegistry serviceRegistry) {
-
+            final Class<?> entityClass) {
         super(EntityFacet.class, holder, Precedence.HIGH);
+        getServiceInjector().injectServicesInto(this);
+
         this.entityClass = entityClass;
-        this.serviceRegistry = serviceRegistry;
-        this.idStringifierService = serviceRegistry.lookupServiceElseFail(IdStringifierService.class);
+        this.primaryKeyType = idStringifierLookupService
+                .primaryKeyTypeFor(entityClass, getPrimaryKeyType());
     }
 
     // -- ENTITY FACET
@@ -91,7 +94,7 @@ public class JpaEntityFacet
 
         return Optional.ofNullable(primaryKeyIfAny)
                 .map(primaryKey->
-                    idStringifierService.enstringPrimaryKey(getPrimaryKeyType(), primaryKey));
+                    primaryKeyType.enstringWithCast(primaryKey));
     }
 
     @Override
@@ -99,8 +102,7 @@ public class JpaEntityFacet
 
         log.debug("fetchEntity; bookmark={}", bookmark);
 
-        val primaryKey = idStringifierService
-                .destringPrimaryKey(getPrimaryKeyType(), entityClass, bookmark.getIdentifier());
+        val primaryKey = primaryKeyType.destring(bookmark.getIdentifier());
 
         val entityManager = getEntityManager();
         val entityPojo = entityManager.find(entityClass, primaryKey);
@@ -311,12 +313,8 @@ public class JpaEntityFacet
 
     // -- DEPENDENCIES
 
-    protected JpaContext getJpaContext() {
-        return serviceRegistry.lookupServiceElseFail(JpaContext.class);
-    }
-
     protected EntityManager getEntityManager() {
-        return getJpaContext().getEntityManagerByManagedType(entityClass);
+        return jpaContext.getEntityManagerByManagedType(entityClass);
     }
 
     protected PersistenceUnitUtil getPersistenceUnitUtil(final EntityManager entityManager) {
diff --git a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacetFactory.java b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacetFactory.java
index ca1ed4ed6d..cf482fab56 100644
--- a/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacetFactory.java
+++ b/persistence/jpa/integration/src/main/java/org/apache/isis/persistence/jpa/integration/entity/JpaEntityFacetFactory.java
@@ -49,7 +49,7 @@ extends FacetFactoryAbstract {
         }
 
         addFacet(
-                new JpaEntityFacet(facetHolder, cls, getServiceRegistry()));
+                new JpaEntityFacet(facetHolder, cls));
     }
 
 }
diff --git a/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java b/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
index 6608afeb62..72d006b5d1 100644
--- a/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
+++ b/viewers/restfulobjects/rendering/src/main/java/org/apache/isis/viewer/restfulobjects/rendering/service/valuerender/JsonValueEncoderServiceDefault.java
@@ -127,7 +127,7 @@ public class JsonValueEncoderServiceDefault implements JsonValueEncoderService {
             final ValueSerializer<?> valueSerializer) {
         if (valueRepr.isString()) {
             val recoveredValue = Try.call(()->
-                    valueSerializer.fromEncodedString(Format.JSON, valueRepr.asString()))
+                    valueSerializer.destring(Format.JSON, valueRepr.asString()))
                     .mapFailure(ex->_Exceptions
                             .illegalArgument(ex, "Unable to parse value %s as String", valueRepr))
                     .ifFailureFail()
@@ -235,7 +235,7 @@ public class JsonValueEncoderServiceDefault implements JsonValueEncoderService {
 
         // else
         return Facets.valueSerializerElseFail(objectSpec, cls)
-                .toEncodedString(Format.JSON, _Casts.uncheckedCast(adapter.getPojo()));
+                .enstring(Format.JSON, _Casts.uncheckedCast(adapter.getPojo()));
     }
 
     /**
diff --git a/viewers/wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/util/PageParameterUtils.java b/viewers/wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/util/PageParameterUtils.java
index 44075d5f0b..3fe8b00bfe 100644
--- a/viewers/wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/util/PageParameterUtils.java
+++ b/viewers/wicket/model/src/main/java/org/apache/isis/viewer/wicket/model/util/PageParameterUtils.java
@@ -33,10 +33,8 @@ import org.springframework.lang.Nullable;
 import org.apache.isis.applib.Identifier;
 import org.apache.isis.applib.services.bookmark.Bookmark;
 import org.apache.isis.commons.collections.Can;
-import org.apache.isis.commons.internal.base._Casts;
 import org.apache.isis.commons.internal.base._Strings;
 import org.apache.isis.core.metamodel.context.MetaModelContext;
-import org.apache.isis.core.metamodel.facets.object.value.ValueSerializer.Format;
 import org.apache.isis.core.metamodel.object.ManagedObject;
 import org.apache.isis.core.metamodel.object.ManagedObjects;
 import org.apache.isis.core.metamodel.spec.ObjectSpecification;
@@ -210,13 +208,6 @@ public class PageParameterUtils {
         if(adapter == null) {
             return NULL_ARG;
         }
-
-        final ObjectSpecification objSpec = adapter.getSpecification();
-        if(objSpec.isValue()) {
-            return Facets.valueSerializerElseFail(objSpec, objSpec.getCorrespondingClass())
-            .toEncodedString(Format.JSON, _Casts.uncheckedCast(adapter.getPojo()));
-        }
-
         return ManagedObjects.stringify(adapter).orElse(null);
     }
 
@@ -227,13 +218,6 @@ public class PageParameterUtils {
         if(NULL_ARG.equals(encoded)) {
             return null;
         }
-
-        if(objSpec.isValue()) {
-            return ManagedObject.value(objSpec,
-                    Facets.valueSerializerElseFail(objSpec, objSpec.getCorrespondingClass())
-                        .fromEncodedString(Format.JSON, encoded));
-        }
-
         try {
             return Bookmark.parseUrlEncoded(encoded)
                     .flatMap(mmc::loadObject)