Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Questions about custom types #180

Open
stuarthalloway opened this issue Nov 12, 2024 · 3 comments
Open

Questions about custom types #180

stuarthalloway opened this issue Nov 12, 2024 · 3 comments

Comments

@stuarthalloway
Copy link

Hello. I am trying to understand Nippy's custom types, and have two questions:

  1. If you specify a custom freeze and later read without a custom thaw, what happens? Is there a default handler mechanism?
  2. Given that Nippy delegates (sometimes?) to edn and Java serialization which have their own customization mechanisms, how are customizations prioritized, e.g. what would happen if you wrote an instance that had customized Java serialization, Clojure print, and Nippy freeze?

Thanks much!

@ptaoussanis
Copy link
Member

ptaoussanis commented Nov 12, 2024

Hi Stuart!

1. If you specify a custom freeze and later read without a custom thaw, what happens? Is there a default handler mechanism?

If you specify a custom freeze implementation with extend-freeze but don't provide the corresponding thaw implementation with extend-thaw, then Nippy will throw an error when you try thaw:

(defrecord MyRec [data])
(extend-freeze MyRec 1 [x out] (.writeUTF out (:data x)))
;; (extend-thaw 1 [in] (MyRec. (.readUTF in))) ; Disabled
(thaw (freeze (MyRec. "Foo")))

Nippy freezes values using this protocol fn.

Each implementation writes a type id (byte) to the output stream, and a possible payload (byte array). Freezing a small string will write byte 96 (type id for small strings), and the string's payload (string length, and UTF8 bytes).

Nippy then thaws based on the written type id.

So if a value was written with a custom type id, the same type id will need a registered implementation on thaw.

2. Given that Nippy delegates (sometimes?) to edn and Java serialization which have their own customization mechanisms, how are customizations prioritized, e.g. what would happen if you wrote an instance that had customized Java serialization, Clojure print, and Nippy freeze?

  • If there is an implementation of the value's type for Nippy's mentioned protocol fn, then that implementation will always be used for freezing - and must be available also for thawing. In this (common) case neither freezing nor thawing will attempt to use Java serialization or Clojure's print/read.

  • If there is not an implementation of the value's type for Nippy's mentioned protocol fn when freezing - then the protocol's Object implementation kicks in - which (depending on options) may attempt to use Java's Serializable or Clojure's print.

So whatever method is ultimately used during freezing - will determine the type id written, and so the method that will (and must) be used during thawing.

Hope that helps?

@stuarthalloway
Copy link
Author

Hi Peter!

Wow, thanks for the thoughtful, comprehensive, and fast response! I want to document a pattern for using Nippy such that consumers can never be broken for lack of a custom handler. It seems that this is possible as follows:

  1. Customize at the edn reader level instead of using Nippy's freeze/thaw customization.
  2. Make sure that nothing ever drops to Java serialization because one cannot trust classes not to do weird stuff like Externalizable. (Browsing docs...) Maybe by using an empty *freeze-serializable-allowlist*? Or a custom *freeze-fallback* that delegates to edn but never to Java serialization?

Does that suffice to lock all the trapdoors?

I presume limiting Nippy in this way has performance (and maybe other?) tradeoffs, but that those tradeoffs are not so severe that they erase the advantages of Nippy over e.g. edn.

@ptaoussanis
Copy link
Member

No worries Stuart, you're very welcome!

Your proposed pattern sounds generally reasonable to me 👍

Re: your point 2 - both options should work, but the simpler is probably to ensure that *freeze-serializable-allowlist* is an empty set. That'll mean freezing will use Nippy's freeze protocol fn (when there's an implementation for the type), otherwise Clojure's pr-str (when the type seems to support pr+read), otherwise Nippy will throw.

Does that suffice to lock all the trapdoors?

Depending on the context of where/how your pattern may be used, there might be a couple things to keep in mind:

  1. If the user has a custom *freeze-fallback* in place, then fallback (non-protocol) behaviour wouldn't be under your control. So in addition to setting *freeze-serializable-allowlist*, you may also want to ensure that *freeze-fallback* is nil.

  2. Other code on the user's system could extend Nippy's protocol. If that happens and custom types are written - then you're back to needing the thawing system to have the same extensions. It'd be quite the anti-pattern for libraries to do this, and I'm not aware of any that do - but it is at least conceivable.

If you are concerned about (2) and are able to share a little more about your context, I might be able to propose some ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants