Skip to content

Commit 99c7074

Browse files
authored
Set allow_duplicate_key: true for all JSON.parse call (#5410)
**Which issue(s) this PR fixes**: Fixes # **What this PR does / why we need it**: The json gem emits a deprecation warning for duplicate keys in JSON objects and will raise `JSON::ParserError` in json 3.0 unless `allow_duplicate_key: true` is specified. Fluentd has historically accepted duplicate keys silently (last value wins) on every path: Yajl-based stream parsers, plain `JSON.parse` before json 2.10, and Oj. To preserve this behavior uniformly regardless of the json gem version, introduce `Fluent::DEFAULT_JSON_PARSE_OPTIONS` and pass it to all `JSON.parse`. Ref. https://github.com/ruby/json/blob/master/CHANGES.md#2025-09-18-2140 **Docs Changes**: N/A **Release Note**: * Set `allow_duplicate_key: true` for all `JSON.parse` call Signed-off-by: Shizuo Fujita <fujita@clear-code.com>
1 parent 513ccd2 commit 99c7074

13 files changed

Lines changed: 38 additions & 15 deletions

File tree

lib/fluent/command/cat.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def abort_message(time, record)
326326
when 'json'
327327
begin
328328
while line = $stdin.gets
329-
record = JSON.parse(line)
329+
record = JSON.parse(line, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
330330
w.write(record)
331331
end
332332
rescue

lib/fluent/config/literal_parser.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
require 'socket'
2222
require 'ripper'
2323

24+
require 'fluent/env'
2425
require 'fluent/config/basic_parser'
2526

2627
module Fluent
@@ -241,7 +242,7 @@ def scan_json(is_array)
241242
# '{"foo":"bar", #' -> '{"foo":"bar"}' (to check)
242243
parsed = nil
243244
begin
244-
parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"))
245+
parsed = JSON.parse(buffer + line_buffer.rstrip.sub(/,$/, '') + (is_array ? "]" : "}"), Fluent::DEFAULT_JSON_PARSE_OPTIONS)
245246
rescue JSON::ParserError
246247
# This '#' is in json string literals
247248
end
@@ -276,7 +277,7 @@ def scan_json(is_array)
276277

277278
line_buffer << char
278279
begin
279-
result = JSON.parse(buffer + line_buffer)
280+
result = JSON.parse(buffer + line_buffer, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
280281
rescue JSON::ParserError
281282
# Incomplete json string yet
282283
end

lib/fluent/config/types.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ def self.hash_value(val, opts = {}, name = nil)
201201
return nil if val.nil?
202202

203203
param = if val.is_a?(String)
204-
val.start_with?('{') ? JSON.parse(val) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
204+
val.start_with?('{') ? JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS) : Hash[val.strip.split(/\s*,\s*/).map{|v| v.split(':', 2)}]
205205
else
206206
val
207207
end
@@ -228,7 +228,7 @@ def self.array_value(val, opts = {}, name = nil)
228228
return nil if val.nil?
229229

230230
param = if val.is_a?(String)
231-
val.start_with?('[') ? JSON.parse(val) : val.strip.split(/\s*,\s*/)
231+
val.start_with?('[') ? JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS) : val.strip.split(/\s*,\s*/)
232232
else
233233
val
234234
end

lib/fluent/configurable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515
#
1616

17+
require 'fluent/env'
1718
require 'fluent/config/configure_proxy'
1819
require 'fluent/config/section'
1920
require 'fluent/config/error'

lib/fluent/daemon.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
$LOAD_PATH << File.expand_path(File.join(here, '..'))
66

77
require 'serverengine'
8+
require 'fluent/env'
89
require 'fluent/supervisor'
910

1011
server_module = Fluent.const_get(ARGV[0])
1112
worker_module = Fluent.const_get(ARGV[1])
12-
params = JSON.parse(ARGV[2])
13+
params = JSON.parse(ARGV[2], Fluent::DEFAULT_JSON_PARSE_OPTIONS)
1314
ServerEngine::Daemon.run_server(server_module, worker_module) { Fluent::Supervisor.serverengine_config(params) }

lib/fluent/env.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ module Fluent
2626
DEFAULT_SOCKET_PATH = ENV['FLUENT_SOCKET'] || '/var/run/fluent/fluent.sock'
2727
DEFAULT_BACKUP_DIR = ENV['FLUENT_BACKUP_DIR'] || '/tmp/fluent'
2828
DEFAULT_OJ_OPTIONS = Fluent::OjOptions.load_env
29+
DEFAULT_JSON_PARSE_OPTIONS = { allow_duplicate_key: true }.freeze
2930
DEFAULT_DIR_PERMISSION = 0755
3031
DEFAULT_FILE_PERMISSION = 0644
3132
INSTANCE_ID = ENV['FLUENT_INSTANCE_ID'] || SecureRandom.uuid

lib/fluent/plugin/filter_record_transformer.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
require 'json'
1919
require 'ostruct'
2020

21+
require 'fluent/env'
2122
require 'fluent/plugin/filter'
2223
require 'fluent/config/error'
2324
require 'fluent/event'
@@ -116,7 +117,7 @@ def filter_stream(tag, es)
116117

117118
def parse_value(value_str)
118119
if value_str.start_with?('{', '[')
119-
JSON.parse(value_str)
120+
JSON.parse(value_str, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
120121
else
121122
value_str
122123
end

lib/fluent/plugin/in_monitor_agent.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
require 'json'
1818

19+
require 'fluent/env'
1920
require 'fluent/config/types'
2021
require 'fluent/plugin/input'
2122
require 'fluent/plugin/output'
@@ -101,7 +102,7 @@ def render_json(obj, code: 200, pretty_json: nil)
101102
end
102103

103104
def render_ltsv(obj, code: 200)
104-
normalized = JSON.parse(obj.to_json)
105+
normalized = JSON.parse(obj.to_json, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
105106
text = ''
106107
normalized.each do |hash|
107108
row = []

lib/fluent/plugin/in_sample.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
require 'json'
1818

19+
require 'fluent/env'
1920
require 'fluent/plugin/input'
2021
require 'fluent/config/error'
2122

@@ -44,7 +45,7 @@ class SampleInput < Input
4445
desc "The sample data to be generated. An array of JSON hashes or a single JSON hash."
4546
config_param :sample, alias: :dummy, default: [{"message" => "sample"}] do |val|
4647
begin
47-
parsed = JSON.parse(val)
48+
parsed = JSON.parse(val, Fluent::DEFAULT_JSON_PARSE_OPTIONS)
4849
rescue JSON::ParserError => ex
4950
# Fluent::ConfigParseError, "got incomplete JSON" will be raised
5051
# at literal_parser.rb with --use-v1-config, but I had to

lib/fluent/plugin/parser_json.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515
#
1616

17+
require 'fluent/env'
1718
require 'fluent/plugin/parser'
1819
require 'fluent/time'
1920
require 'fluent/oj_options'
@@ -38,6 +39,10 @@ class JSONParser < Parser
3839

3940
config_set_default :time_type, :float
4041

42+
# Use a shared proc rather than a per-call lambda so that
43+
# configure_json_parser returns the same object every time.
44+
JSON_PARSE_PROC = ->(text) { JSON.parse(text, Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
45+
4146
def configure(conf)
4247
if conf.has_key?('time_format')
4348
conf['time_type'] ||= 'string'
@@ -54,7 +59,7 @@ def configure_json_parser(name)
5459

5560
log&.info "Oj is not installed, and failing back to JSON for json parser"
5661
configure_json_parser(:json)
57-
when :json then [JSON.method(:parse), JSON::ParserError]
62+
when :json then [JSON_PARSE_PROC, JSON::ParserError]
5863
when :yajl then [Yajl.method(:load), Yajl::ParseError]
5964
else
6065
raise "BUG: unknown json parser specified: #{name}"

0 commit comments

Comments
 (0)