lib/active_support/core_ext/object/json.rb



# frozen_string_literal: true

# Hack to load JSON gem first so we can override its to_json.
require "json"
require "bigdecimal"
require "ipaddr"
require "uri/generic"
require "pathname"
require "active_support/core_ext/big_decimal/conversions" # for #to_s
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
require "active_support/core_ext/object/instance_variables"
require "time"
require "active_support/core_ext/time/conversions"
require "active_support/core_ext/date_time/conversions"
require "active_support/core_ext/date/conversions"

#--
# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
# their default behavior. That said, we need to define the basic to_json method in all of them,
# otherwise they will always use to_json gem implementation, which is backwards incompatible in
# several cases (for instance, the JSON implementation for Hash does not work) with inheritance.
#
# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
# calls to the original to_json method.
#
# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
# should give exactly the same results with or without Active Support.

module ActiveSupport
  module ToJsonWithActiveSupportEncoder # :nodoc:
    def to_json(options = nil)
      if options.is_a?(::JSON::State)
        # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
        super(options)
      else
        # to_json is being invoked directly, use ActiveSupport's encoder
        ActiveSupport::JSON.encode(self, options)
      end
    end
  end
end

[Enumerable, Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].reverse_each do |klass|
  klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
end

class Module
  def as_json(options = nil) # :nodoc:
    name
  end
end

class Object
  def as_json(options = nil) # :nodoc:
    if respond_to?(:to_hash)
      to_hash.as_json(options)
    else
      instance_values.as_json(options)
    end
  end
end

if RUBY_VERSION >= "3.2"
  class Data # :nodoc:
    def as_json(options = nil)
      to_h.as_json(options)
    end
  end
end

class Struct # :nodoc:
  def as_json(options = nil)
    to_h.as_json(options)
  end
end

class TrueClass
  def as_json(options = nil) # :nodoc:
    self
  end
end

class FalseClass
  def as_json(options = nil) # :nodoc:
    self
  end
end

class NilClass
  def as_json(options = nil) # :nodoc:
    self
  end
end

class String
  def as_json(options = nil) # :nodoc:
    self
  end
end

class Symbol
  def as_json(options = nil) # :nodoc:
    to_s
  end
end

class Numeric
  def as_json(options = nil) # :nodoc:
    self
  end
end

class Float
  # Encoding Infinity or NaN to JSON should return "null". The default returns
  # "Infinity" or "NaN" which are not valid JSON.
  def as_json(options = nil) # :nodoc:
    finite? ? self : nil
  end
end

class BigDecimal
  # A BigDecimal would be naturally represented as a JSON number. Most libraries,
  # however, parse non-integer JSON numbers directly as floats. Clients using
  # those libraries would get in general a wrong number and no way to recover
  # other than manually inspecting the string with the JSON code itself.
  #
  # That's why a JSON string is returned. The JSON literal is not numeric, but
  # if the other end knows by contract that the data is supposed to be a
  # BigDecimal, it still has the chance to post-process the string and get the
  # real value.
  def as_json(options = nil) # :nodoc:
    finite? ? to_s : nil
  end
end

class Regexp
  def as_json(options = nil) # :nodoc:
    to_s
  end
end

module Enumerable
  def as_json(options = nil) # :nodoc:
    to_a.as_json(options)
  end
end

class IO
  def as_json(options = nil) # :nodoc:
    to_s
  end
end

class Range
  def as_json(options = nil) # :nodoc:
    to_s
  end
end

class Array
  def as_json(options = nil) # :nodoc:
    map { |v| options ? v.as_json(options.dup) : v.as_json }
  end
end

class Hash
  def as_json(options = nil) # :nodoc:
    # create a subset of the hash by applying :only or :except
    subset = if options
      if attrs = options[:only]
        slice(*Array(attrs))
      elsif attrs = options[:except]
        except(*Array(attrs))
      else
        self
      end
    else
      self
    end

    result = {}
    subset.each do |k, v|
      result[k.to_s] = options ? v.as_json(options.dup) : v.as_json
    end
    result
  end
end

class Time
  def as_json(options = nil) # :nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
    end
  end
end

class Date
  def as_json(options = nil) # :nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      strftime("%Y-%m-%d")
    else
      strftime("%Y/%m/%d")
    end
  end
end

class DateTime
  def as_json(options = nil) # :nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      strftime("%Y/%m/%d %H:%M:%S %z")
    end
  end
end

class URI::Generic # :nodoc:
  def as_json(options = nil)
    to_s
  end
end

class Pathname # :nodoc:
  def as_json(options = nil)
    to_s
  end
end

class IPAddr # :nodoc:
  def as_json(options = nil)
    to_s
  end
end

class Process::Status # :nodoc:
  def as_json(options = nil)
    { exitstatus: exitstatus, pid: pid }
  end
end

class Exception
  def as_json(options = nil)
    to_s
  end
end