Course outline

Ruby / Integration: data, resources, runtime evidence, and trust / Lesson 13

Turn decoded JSON into a validated archive record

Separate syntax decoding, String-keyed structure, domain limits, explicit field selection, and safe rejection.

About 5 min readContent reviewed
On this page

Lesson goals

  • Explain why valid JSON and permissive Ruby objects do not establish a trusted command.
  • Validate required types and ranges before constructing an internal representation.

Parsing is only the first boundary

JSON.parse produces ordinary Ruby values such as hashes, arrays, strings, numbers, true, false, and nil. A syntactically valid value can still be the wrong top-level shape or contain unacceptable fields. Ordinary parsed object keys are Strings unless options deliberately change that policy. Keep decoding close to a validator rather than scattering permissive hash access through domain code. Use JSON parsing without object additions for untrusted data, and do not substitute a general object-restoration mechanism for a data decoder. Library availability and version belong to the actual runtime and dependency environment.

Check the whole required contract before trusting the result

The validator accepts a Hash with a nonblank String identifier and an Integer copy count from one to fifty. That limit is a fictional archive policy, not a universal tuning or safety threshold. It returns a frozen internal hash with selected symbol keys, but freezing the outer hash is not a deep-immutability guarantee for every possible value. The normalized identifier is a new String from strip and remains subject to the model’s ownership policy. A real operation still requires identity, resource authorization, authoritative state, and concurrency checks beyond these local field rules.

ruby Decode first, then validate a selected internal schema
require 'json'
def validate_record(value)
  return nil unless value.is_a?(Hash)
  id = value['id']
  copies = value['copies']
  return nil unless id.is_a?(String) && copies.is_a?(Integer)
  normalized = id.strip
  return nil if normalized.empty? || copies < 1 || copies > 50
  {id: normalized, copies: copies}.freeze
end
value = JSON.parse('{"id":" A-4 ","copies":2}', create_additions: false)
record = validate_record(value)
puts(record ? "#{record.fetch(:id)}:#{record.fetch(:copies)}" : 'invalid')
puts validate_record({'id' => 'A-5', 'copies' => '2'}).nil?
Expected output
A-4:2
true

Expected output is A-4:2 then true. A string count is rejected rather than silently converted. The program requires the JSON library and was not run in this environment.

Make coercion and unknown-field policy explicit

Do not automatically stringify every identifier or call to_i on every count to make validation pass. Such coercion can conceal sender defects and erase rejection causes. Decide whether unknown fields are ignored, rejected, or retained in a bounded extension area, and whether missing and null have different meanings. Duplicate source keys may require a parser-level policy before an ordinary hash has already collapsed their representation. Preserve per-record outcomes if partial acceptance is allowed. Normalization must also be declared: stripping human-entered text can be reasonable while altering signed or case-sensitive identifiers can be wrong.

Encode only the public representation

JSON.generate handles string escaping and supported data values without manual concatenation. Build a selected public map instead of serializing all internal state. The example omits a local note and uses String keys for an explicit external schema. This is data selection, not a full privacy or authorization proof. Numeric precision must match downstream consumers, especially when large Ruby integers cross a JavaScript or fixed-width boundary. A valid JSON string can still carry the wrong meaning, so review field names, units, ranges, and version evolution as part of the API contract.

ruby An explicit public JSON shape
require 'json'
internal = {id: 'A-4', copies: 2, note: 'local-only'}
public_value = {'id' => internal.fetch(:id), 'copies' => internal.fetch(:copies)}
puts JSON.generate(public_value)
Expected output
{"id":"A-4","copies":2}

Expected output is {"id":"A-4","copies":2}. note is intentionally absent. No external record or service is accessed, and no Ruby encoding test was executed.

Bound input and preserve safe diagnostics

A convenient parser does not impose the product’s byte-size, nesting, list-length, or processing limits automatically. Enforce appropriate bounds at the receiving layer, use declared parser options, and avoid logging whole rejected documents. Catch JSON::ParserError at the decoding boundary rather than swallowing unrelated application failures. Test wrong top-level values, missing and nil fields, mixed arrays, boundary counts, and normalization in an isolated selected runtime. This course can check embedded JSON as data, but it executed no Ruby JSON parser, gem loading, authorization, or integration test.

Common errors and fixes

A valid JSON document is accepted with the wrong field types.

Why it happens
Decoding was mistaken for schema and domain validation.
How to fix it
Validate shape, required types, and ranges explicitly.
Prevent it next time
Keep parsed values untrusted until the validated model is built.

A symbol-keyed lookup misses a parsed JSON field.

Why it happens
The internal and external key schemas were conflated.
How to fix it
Translate known fields once through a validated boundary.
Prevent it next time
Use fixtures with the actual parser’s key representation.

An internal field leaks into serialized output.

Why it happens
All available state was encoded instead of a selected public contract.
How to fix it
Build an explicit representation with only declared fields.
Prevent it next time
Review output, logging, and authorization boundaries together.

Check your understanding

1. Why not call to_i on the JSON string "2x" and accept the result?

Show worked solution

It can turn malformed text into a plausible quantity and hide the sender’s type or grammar error. Permit coercion only under a precise public contract with defined accepted forms and failure behavior. Otherwise reject the wrong type, and validate the numeric range separately. A convenient conversion is not evidence that the input meant what the application assumes.

2. Does the frozen validated hash authorize a real archive modification?

Show worked solution

No. It records only local field checks and an outer-container mutation restriction. The caller’s identity, resource ownership, current state, allowed action, and concurrent update rules remain separate. Route real commands through their authoritative owner and verify those guarantees independently. Do not turn a data-transfer object into an accidental authorization decision.

Key takeaways

  • Decode, validate, authorize, and execute are separate responsibilities.
  • Explicit key, coercion, and output policies preserve JSON meaning.

Official references