Module: Familia::Horreum::Serialization

Included in:
Familia::Horreum
Defined in:
lib/familia/horreum/serialization.rb

Overview

Serialization - Instance-level methods for object serialization Handles conversion between Ruby objects and Valkey hash storage

Instance Method Summary collapse

Instance Method Details

#debug_fieldsHash{String => Hash}

Returns a diagnostic hash showing Ruby values vs stored JSON side-by-side.

Useful for debugging double-encoding issues or understanding the serialization boundary. Each field maps to a hash showing the Ruby value, the JSON string that would be stored, and the Ruby type.

Examples:

user.debug_fields
# => {
#   "name"    => { ruby: "UK",  stored: "\"UK\"",  type: "String"  },
#   "age"     => { ruby: 30,    stored: "30",      type: "Integer" },
#   "active"  => { ruby: true,  stored: "true",    type: "TrueClass" },
#   "email"   => { ruby: nil,   stored: nil,       type: "NilClass" }
# }

Returns:

  • (Hash{String => Hash})

    Each field name maps to:

    • :ruby [Object] the current in-memory Ruby value
    • :stored [String] the JSON-encoded string for Redis storage
    • :type [String] the Ruby class name of the value


110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/familia/horreum/serialization.rb', line 110

def debug_fields
  self.class.persistent_fields.each_with_object({}) do |field, hsh|
    field_type = self.class.field_types[field]
    val = send(field_type.method_name)
    stored = serialize_value(val)

    hsh[field.to_s] = {
      ruby: val,
      stored: stored,
      type: val.class.name,
    }
  end
end

#deserialize_value(val, symbolize: false, field_name: nil) ⇒ Object

Converts a Redis string value back to its original Ruby type

This method deserializes JSON strings back to their original Ruby types (Integer, Boolean, Float, nil, Hash, Array). Plain strings that cannot be parsed as JSON are returned as-is.

This pairs with serialize_value which JSON-encodes all non-string values. The contract ensures type preservation across Redis storage:

  • Strings stored as-is → returned as-is
  • All other types JSON-encoded → JSON-decoded back to original type

Parameters:

  • val (String)

    The string value from Redis to deserialize

  • symbolize (Boolean) (defaults to: false)

    Whether to symbolize hash keys (default: false)

  • field_name (Symbol, nil) (defaults to: nil)

    Optional field name for better error context

Returns:

  • (Object)

    The deserialized value with original Ruby type, or the original string if not JSON



209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/familia/horreum/serialization.rb', line 209

def deserialize_value(val, symbolize: false, field_name: nil)
  return nil if val.nil? || val == ''

  # Handle Redis::Future objects during transactions
  return val if val.is_a?(Redis::Future)

  begin
    Familia::JsonSerializer.parse(val, symbolize_names: symbolize)
  rescue Familia::SerializerError
    log_deserialization_issue(val, field_name)
    val
  end
end

#serialize_value(val) ⇒ String

Note:

Strings are JSON-encoded to prevent type coercion bugs where string "123" would be indistinguishable from integer 123 in storage

Note:

This method integrates with Familia's type system and supports custom serialization methods when available on the object

Serializes a Ruby object for Valkey storage.

Converts ALL Ruby values (including strings) to JSON-encoded strings for type-safe storage. This ensures round-trip type preservation: the type you save is the type you get back.

The serialization process:

  1. ConcealedStrings (encrypted values) → extract encrypted_value
  2. ALL other types → JSON serialization (String, Integer, Boolean, Float, nil, Hash, Array)

Examples:

Type preservation through JSON encoding

serialize_value("007")           # => "\"007\"" (JSON string)
serialize_value(123)             # => "123" (JSON number)
serialize_value(true)            # => "true" (JSON boolean)
serialize_value({a: 1})          # => "{\"a\":1}" (JSON object)

Parameters:

  • val (Object)

    The Ruby object to serialize for Valkey storage

Returns:

  • (String)

    JSON-encoded string representation

See Also:

  • For extracting identifiers from Familia objects


184
185
186
187
188
189
190
191
# File 'lib/familia/horreum/serialization.rb', line 184

def serialize_value(val)
  # Security: Handle ConcealedString safely - extract encrypted data for storage
  return val.encrypted_value if val.respond_to?(:encrypted_value)

  # ALWAYS write valid JSON for type preservation
  # This includes strings, which get JSON-encoded with wrapping quotes
  Familia::JsonSerializer.dump(val)
end

#to_aArray

Note:

Values are serialized using the same process as other persistence methods to maintain data consistency across operations.

Converts the object's persistent fields to an array.

Serializes all persistent field values in field definition order, preparing them for Valkey storage. Each value is processed through the serialization pipeline to ensure Valkey compatibility.

Examples:

Converting an object to array format

user = User.new(name: "John", email: "john@example.com", age: 30)
user.to_a
# => ["John", "john@example.com", "30"]

Returns:

  • (Array)

    Array of serialized field values in field order



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/familia/horreum/serialization.rb', line 140

def to_a
  self.class.persistent_fields.map do |field|
    field_type = self.class.field_types[field]

    # Security: Skip non-loggable fields (e.g., encrypted fields)
    next unless field_type.loggable

    method_name = field_type.method_name
    val = send(method_name)
    Familia.debug " [to_a] field: #{field} method: #{method_name} val: #{val.class}"

    # Return actual Ruby values, including nil to maintain array positions
    val
  end
end

#to_hHash

Note:

Only loggable fields are included. Encrypted fields are excluded.

Note:

Every persistent field appears, including those whose value is nil. This differs from #to_h_for_storage, which omits nil fields so that absence -- not a stored "null" -- represents "no value" in Valkey/Redis.

Converts the object's persistent fields to a hash for external use.

Returns actual Ruby values (String, Integer, Hash, etc.) for API consumption, NOT JSON-encoded strings. Excludes non-loggable fields like encrypted fields for security.

Examples:

Converting an object to hash format for API response

user = User.new(name: "John", email: "john@example.com", age: 30)
user.to_h
# => {"name"=>"John", "email"=>"john@example.com", "age"=>30}
# Note: Returns actual Ruby types, not JSON strings

Returns:

  • (Hash)

    Hash with field names as string keys and Ruby values



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/familia/horreum/serialization.rb', line 29

def to_h
  self.class.persistent_fields.each_with_object({}) do |field, hsh|
    field_type = self.class.field_types[field]

    # Security: Skip non-loggable fields (e.g., encrypted fields)
    next unless field_type.loggable

    val = send(field_type.method_name)
    Familia.debug " [to_h] field: #{field} val: #{val.class}"

    # Use string key for external API compatibility
    # Return Ruby values, not JSON-encoded strings
    hsh[field.to_s] = val
  end
end

#to_h_for_storageHash

Note:

This is an internal method used by commit_fields and hmset

Note:

Nil-valued fields are omitted so that field absence -- not a stored "null" -- represents "no value" in the Valkey/Redis hash. This is what keeps HSETNX/HEXISTS-based claim patterns usable and avoids materializing every declared field. Fields cleared to nil are actively removed from storage on save; see Persistence#remove_stale_nil_fields.

Converts the object's persistent fields to a hash for database storage.

Returns JSON-encoded strings for ALL persistent field values, ready for Redis storage. Unlike to_h, this includes encrypted fields and serializes values using serialize_value (JSON encoding).

Examples:

Internal storage preparation

user = User.new(name: "John", age: 30)
user.to_h_for_storage
# => {"name"=>"\"John\"", "age"=>"30"}
# Note: Strings are JSON-encoded with quotes

Returns:

  • (Hash)

    Hash with field names as string keys and JSON-encoded values



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/familia/horreum/serialization.rb', line 66

def to_h_for_storage
  self.class.persistent_fields.each_with_object({}) do |field, hsh|
    field_type = self.class.field_types[field]

    val = send(field_type.method_name)

    # Omit nil fields entirely. A Valkey/Redis hash has no NULL: absence is
    # the native "no value". Persisting nil as the JSON string "null" would
    # make declared fields perpetually present, defeating HSETNX/HEXISTS and
    # wasting memory. Round-trips are unaffected -- deserialize_value already
    # returns nil for an absent field.
    next if val.nil?

    prepared = serialize_value(val)

    if Familia.debug?
      Familia.debug " [to_h_for_storage] field: #{field} val: #{val.class} prepared: #{prepared&.class || '[nil]'}"
    end

    # Use string key for database compatibility
    hsh[field.to_s] = prepared
  end
end