Replace ad-hoc JSON parsing/serialising with oatpp ObjectMapper #6

Closed
opened 2026-04-25 13:24:10 +02:00 by uwe.admin · 7 comments
Owner

Location: JsonErrorHandler.hpp, AuthInterceptor::makeJsonError, ws/Listener::jsonStr/jsonInt, Hub::buildPresenceMsg

Multiple sites hand-assemble JSON via string concatenation. JsonErrorHandler::handleError embeds status.description into the body without escaping, the ad-hoc jsonStr/jsonInt parsers in ws/Listener.hpp mishandle escaped quotes and nested objects. Switch to oatpp::parser::json::mapping::ObjectMapper.

From audit #1.

**Location:** `JsonErrorHandler.hpp`, `AuthInterceptor::makeJsonError`, `ws/Listener::jsonStr/jsonInt`, `Hub::buildPresenceMsg` Multiple sites hand-assemble JSON via string concatenation. `JsonErrorHandler::handleError` embeds `status.description` into the body without escaping, the ad-hoc `jsonStr`/`jsonInt` parsers in `ws/Listener.hpp` mishandle escaped quotes and nested objects. Switch to `oatpp::parser::json::mapping::ObjectMapper`. From audit #1.
Author
Owner

Agent Evaluation

Feasibility: Medium. Multiple call sites (AuthInterceptor::makeJsonError, JsonErrorHandler::handleError, the escapeJson helper, Hub::buildPresenceMsg/notifyBooking/notifyPerson, ws/Listener::jsonStr/jsonInt) all hand-roll JSON. Switching them to oatpp::parser::json::mapping::ObjectMapper removes the escape bugs but means each call site needs a small DTO struct (or a helper that builds an oatpp::Fields<…> map).

Impact: Medium. Concrete bugs the audit identified: JsonErrorHandler::handleError embeds status.description unescaped (a custom Status{418,"I'm a \"teapot\""} produces invalid JSON); escapeJson doesn't handle invalid UTF-8 or nullptr; the ws/Listener jsonStr/jsonInt parsers mishandle escaped quotes and nested objects. Real-world risk is low (status descriptions are usually static, WS message format is internal), but the failure mode is silent corruption rather than a clean error.

Effort: Medium.

Recommendation: Accept.

Implementation plan

  1. Centralise an ObjectMapper singleton (or take one via DI as AuthInterceptor already does) and use it everywhere.
  2. JsonErrorHandler — define a small ErrorDto { String status; String message; }, build it, serialise via the mapper. Drop escapeJson entirely.
  3. AuthInterceptor::makeJsonError — same DTO + mapper. Removes the inline "{\"status\":\"Unauthorized\"}" literals.
  4. Hub::buildPresenceMsg / notifyBooking / notifyPerson — DTOs per message kind, serialised via the mapper. Lets us add fields without touching string concatenation.
  5. ws/Listener jsonStr/jsonInt — replace with mapper-driven DTO parsing. The current ad-hoc parsers mishandle nested objects and escaped quotes; switch to a strongly-typed IncomingWsMsgDto and let the mapper validate.
  6. Tests (in the harness from #2): round-trip test for each DTO; one test that confirms a Status{418, "a\"b"} description survives the round-trip.

Decision needed

Check one (edit this comment):

  • Option A — DI the ObjectMapper everywhere — same pattern as AuthInterceptor's ctor; JsonErrorHandler/Hub/Listener all take a shared_ptr<ObjectMapper>. Most flexible, slightly more wiring per consumer.
  • Option B — Single static ObjectMapper instance inside oatpp_authkit:: — internal helper inline auto& mapper() returning a process-wide oatpp::parser::json::mapping::ObjectMapper. Zero ctor change for consumers; harder to swap out.
  • Option C — Hybrid — Default to a static instance, but every public class also accepts an optional shared_ptr<ObjectMapper> ctor arg for tests / advanced users.

Default recommendation: B. The existing call sites are all internal helpers; consumers don't need to plug in their own mapper. If they ever do, escalate to C.

## Agent Evaluation **Feasibility:** Medium. Multiple call sites (`AuthInterceptor::makeJsonError`, `JsonErrorHandler::handleError`, the `escapeJson` helper, `Hub::buildPresenceMsg`/`notifyBooking`/`notifyPerson`, `ws/Listener::jsonStr`/`jsonInt`) all hand-roll JSON. Switching them to `oatpp::parser::json::mapping::ObjectMapper` removes the escape bugs but means each call site needs a small DTO struct (or a helper that builds an `oatpp::Fields<…>` map). **Impact:** Medium. Concrete bugs the audit identified: `JsonErrorHandler::handleError` embeds `status.description` *unescaped* (a custom `Status{418,"I'm a \"teapot\""}` produces invalid JSON); `escapeJson` doesn't handle invalid UTF-8 or nullptr; the `ws/Listener` jsonStr/jsonInt parsers mishandle escaped quotes and nested objects. Real-world risk is low (status descriptions are usually static, WS message format is internal), but the failure mode is silent corruption rather than a clean error. **Effort:** Medium. **Recommendation:** Accept. ### Implementation plan 1. **Centralise an `ObjectMapper`** singleton (or take one via DI as `AuthInterceptor` already does) and use it everywhere. 2. **`JsonErrorHandler`** — define a small `ErrorDto { String status; String message; }`, build it, serialise via the mapper. Drop `escapeJson` entirely. 3. **`AuthInterceptor::makeJsonError`** — same DTO + mapper. Removes the inline `"{\"status\":\"Unauthorized\"}"` literals. 4. **`Hub::buildPresenceMsg` / `notifyBooking` / `notifyPerson`** — DTOs per message kind, serialised via the mapper. Lets us add fields without touching string concatenation. 5. **`ws/Listener` `jsonStr`/`jsonInt`** — replace with mapper-driven DTO parsing. The current ad-hoc parsers mishandle nested objects and escaped quotes; switch to a strongly-typed `IncomingWsMsgDto` and let the mapper validate. 6. **Tests** (in the harness from #2): round-trip test for each DTO; one test that confirms a `Status{418, "a\"b"}` description survives the round-trip. ### Decision needed Check one (edit this comment): - [x] **Option A — DI the `ObjectMapper` everywhere** — same pattern as `AuthInterceptor`'s ctor; `JsonErrorHandler`/`Hub`/`Listener` all take a `shared_ptr<ObjectMapper>`. Most flexible, slightly more wiring per consumer. - [ ] **Option B — Single static `ObjectMapper` instance inside `oatpp_authkit::`** — internal helper `inline auto& mapper()` returning a process-wide `oatpp::parser::json::mapping::ObjectMapper`. Zero ctor change for consumers; harder to swap out. - [ ] **Option C — Hybrid** — Default to a static instance, but every public class also accepts an optional `shared_ptr<ObjectMapper>` ctor arg for tests / advanced users. Default recommendation: **B**. The existing call sites are all internal helpers; consumers don't need to plug in their own mapper. If they ever do, escalate to C.
uwe.admin added the
evaluated
label 2026-04-25 13:41:07 +02:00
Author
Owner

Evaluated #6 — Medium, recommend accept; A/B/C decision on ObjectMapper wiring (DI / static / hybrid).

Evaluated #6 — Medium, recommend accept; A/B/C decision on ObjectMapper wiring (DI / static / hybrid).
Author
Owner

Agent Evaluation

Feasibility: Mixed. Three distinct hand-rolled JSON sites with very different shapes:

  1. JsonErrorHandler::handleError — emits {"status":...,"message":...} via concatenation; status.description is unescaped (real bug if oatpp ever ships a description with "/\\/control chars), escapeJson handles plain ASCII but not invalid UTF-8 sequences. Easy to swap for an ObjectMapper round-trip on a tiny inline DTO.
  2. AuthInterceptor::makeJsonError — emits the same shape; same fix.
  3. Hub::buildPresenceMsg / notifyBooking / notifyPerson — outbound WS frames; switching to ObjectMapper is straightforward, but it allocates a fresh mapper per call unless we cache one. Acceptable.
  4. Listener::jsonStr / jsonIntinbound parsing of WS messages. These are toy regex-style scanners that miss nested objects, escaped quotes, and unicode escapes. Replacing with ObjectMapper::readFromString<oatpp::Fields<oatpp::String>> (or a tiny purpose-built DTO per message type) is correct but requires defining DTOs for each WS payload shape (type, booking_id, person_id, …). Slightly more invasive than the outbound sites.

Impact: Closes the most consequential parser hazard the audit flagged. The Listener parser is the only place untrusted client input is parsed by hand — getting that onto ObjectMapper makes a class of injection/garbage-input bugs unreachable.

Effort: Medium.

Recommendation: Accept.

Implementation plan

  1. JsonErrorHandler.hpp — define a small inline ErrorDto (status: String, message: String?); use a thread-local oatpp::parser::json::mapping::ObjectMapper (or a static one — these are documented thread-safe in oatpp 1.3) to serialise. Drop escapeJson + the manual concat. Guard against status.description == nullptr.
  2. AuthInterceptor::makeJsonError — same ErrorDto shape; share the static mapper. The makeUnauthorized/makeForbidden callers stay unchanged.
  3. Hub.hpp outbound — define BookingNotifyDto, PersonNotifyDto, PresenceDto. buildPresenceMsg/notifyBooking/notifyPerson serialise via the static mapper.
  4. ws/Listener.hpp inbound — define IncomingWsMessageDto with type: String, booking_id: String?, person_id: String? (one DTO covers all current type values: subscribe_property, unsubscribe_property, view_booking, view_person, idle_view, pong). Replace jsonStr/jsonInt with one readFromString + field access; throw on parse failure → drop the frame (current behaviour).
  5. Header-only constraint: oatpp::parser::json::mapping::ObjectMapper instances are heavier than the current free functions; instantiate once via inline static in each header where used. Document the cost in a comment.
  6. Tests: there is no test suite (audit finding 20). I won't bootstrap one for this PR — the test coverage decision belongs to its own future PR (mirrors what we did for #2). Acceptance becomes "compiles, consumers (fewo-webapp, palibu) keep working without changes".

Decision needed

Check one (edit this comment):

  • Option A — All four sites in one PR — JsonErrorHandler + AuthInterceptor + Hub outbound + Listener inbound; consistent style, larger diff.
  • Option B — Outbound first, Listener separately — Land #1+#2+#3 (the easy outbound serialisation cleanups) in one PR; do #4 (inbound parsing replacement, which adds new DTOs and exercises readFromString error paths) as a follow-up issue. Smaller blast radius per PR.
## Agent Evaluation **Feasibility:** Mixed. Three distinct hand-rolled JSON sites with very different shapes: 1. `JsonErrorHandler::handleError` — emits `{"status":...,"message":...}` via concatenation; `status.description` is unescaped (real bug if oatpp ever ships a description with `"`/`\\`/control chars), `escapeJson` handles plain ASCII but not invalid UTF-8 sequences. Easy to swap for an `ObjectMapper` round-trip on a tiny inline DTO. 2. `AuthInterceptor::makeJsonError` — emits the same shape; same fix. 3. `Hub::buildPresenceMsg` / `notifyBooking` / `notifyPerson` — outbound WS frames; switching to `ObjectMapper` is straightforward, but it allocates a fresh mapper per call unless we cache one. Acceptable. 4. `Listener::jsonStr` / `jsonInt` — *inbound* parsing of WS messages. These are toy regex-style scanners that miss nested objects, escaped quotes, and unicode escapes. Replacing with `ObjectMapper::readFromString<oatpp::Fields<oatpp::String>>` (or a tiny purpose-built DTO per message type) is correct but requires defining DTOs for each WS payload shape (`type`, `booking_id`, `person_id`, …). Slightly more invasive than the outbound sites. **Impact:** Closes the most consequential parser hazard the audit flagged. The `Listener` parser is the only place untrusted client input is parsed by hand — getting that onto `ObjectMapper` makes a class of injection/garbage-input bugs unreachable. **Effort:** Medium. **Recommendation:** Accept. ### Implementation plan 1. **`JsonErrorHandler.hpp`** — define a small inline `ErrorDto` (`status: String`, `message: String?`); use a thread-local `oatpp::parser::json::mapping::ObjectMapper` (or a static one — these are documented thread-safe in oatpp 1.3) to serialise. Drop `escapeJson` + the manual concat. Guard against `status.description == nullptr`. 2. **`AuthInterceptor::makeJsonError`** — same `ErrorDto` shape; share the static mapper. The `makeUnauthorized`/`makeForbidden` callers stay unchanged. 3. **`Hub.hpp` outbound** — define `BookingNotifyDto`, `PersonNotifyDto`, `PresenceDto`. `buildPresenceMsg`/`notifyBooking`/`notifyPerson` serialise via the static mapper. 4. **`ws/Listener.hpp` inbound** — define `IncomingWsMessageDto` with `type: String`, `booking_id: String?`, `person_id: String?` (one DTO covers all current `type` values: `subscribe_property`, `unsubscribe_property`, `view_booking`, `view_person`, `idle_view`, `pong`). Replace `jsonStr`/`jsonInt` with one `readFromString` + field access; throw on parse failure → drop the frame (current behaviour). 5. **Header-only constraint:** `oatpp::parser::json::mapping::ObjectMapper` instances are heavier than the current free functions; instantiate once via `inline static` in each header where used. Document the cost in a comment. 6. **Tests:** there is no test suite (audit finding 20). I won't bootstrap one for this PR — the test coverage decision belongs to its own future PR (mirrors what we did for #2). Acceptance becomes "compiles, consumers (fewo-webapp, palibu) keep working without changes". ### Decision needed Check one (edit this comment): - [x] **Option A — All four sites in one PR** — JsonErrorHandler + AuthInterceptor + Hub outbound + Listener inbound; consistent style, larger diff. - [ ] **Option B — Outbound first, Listener separately** — Land #1+#2+#3 (the easy outbound serialisation cleanups) in one PR; do #4 (inbound parsing replacement, which adds new DTOs and exercises `readFromString` error paths) as a follow-up issue. Smaller blast radius per PR.
uwe.admin added the
effort:medium
label 2026-04-25 13:41:44 +02:00
Author
Owner

Evaluated #6 — Medium, recommend accept; decision checkbox for all-in-one vs outbound-first PR scope.

Evaluated #6 — Medium, recommend accept; decision checkbox for all-in-one vs outbound-first PR scope.
Author
Owner

Re-evaluation

Owner selected:

  • Option A — DI the ObjectMapper everywhere (wiring strategy — first evaluation)
  • Option A — All four sites in one PR (scope — second evaluation)

Acknowledged. Combined plan for the implementation pass:

  1. Constructor-inject std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper> into JsonErrorHandler, AuthInterceptor (already has it), ws::Hub, ws::Listener. Consumers register an ObjectMapper component — same pattern oatpp-authkit already documents for AuthInterceptor.
  2. Replace JsonErrorHandler::handleError hand-rolled body with ObjectMapper::writeToString on a JsonErrorDto { status, message }. Drop the inline escapeJson use there.
  3. Replace AuthInterceptor::makeJsonError similarly.
  4. Replace Hub::buildPresenceMsg, notifyBooking, notifyPerson with mapper-emitted DTOs.
  5. Replace Listener::jsonStr / jsonInt with ObjectMapper::readFromString against per-message-type DTOs (WsClientMsgDto family). Validate error path returns a clean rejection rather than silent corruption.
  6. Remove escapeJson once no remaining call sites; keep if any caller still hand-builds (e.g. logging) — flag in commit.
  7. Add unit tests covering: status with embedded "/\\/control char, invalid UTF-8 input on the WS read path, nested JSON object on the read path.

Apply the accepted label to trigger implementation.

## Re-evaluation Owner selected: - **Option A — DI the `ObjectMapper` everywhere** (wiring strategy — first evaluation) - **Option A — All four sites in one PR** (scope — second evaluation) **Acknowledged.** Combined plan for the implementation pass: 1. Constructor-inject `std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper>` into `JsonErrorHandler`, `AuthInterceptor` (already has it), `ws::Hub`, `ws::Listener`. Consumers register an `ObjectMapper` component — same pattern oatpp-authkit already documents for `AuthInterceptor`. 2. Replace `JsonErrorHandler::handleError` hand-rolled body with `ObjectMapper::writeToString` on a `JsonErrorDto { status, message }`. Drop the inline `escapeJson` use there. 3. Replace `AuthInterceptor::makeJsonError` similarly. 4. Replace `Hub::buildPresenceMsg`, `notifyBooking`, `notifyPerson` with mapper-emitted DTOs. 5. Replace `Listener::jsonStr` / `jsonInt` with `ObjectMapper::readFromString` against per-message-type DTOs (`WsClientMsgDto` family). Validate error path returns a clean rejection rather than silent corruption. 6. Remove `escapeJson` once no remaining call sites; keep if any caller still hand-builds (e.g. logging) — flag in commit. 7. Add unit tests covering: status with embedded `"`/`\\`/control char, invalid UTF-8 input on the WS read path, nested JSON object on the read path. Apply the `accepted` label to trigger implementation.
Author
Owner

Re-evaluated #6 — owner chose DI + all-in-one; awaiting accepted label.

Re-evaluated #6 — owner chose DI + all-in-one; awaiting accepted label.
u.schuster added the
accepted
label 2026-04-25 21:31:06 +02:00
Author
Owner

Implemented #6 → commit f43f5f0, tag v0.4.0 (Option A: DI ObjectMapper across JsonErrorHandler/AuthInterceptor/Hub/Listener; new dto/InternalDto.hpp with 4 message DTOs; 4 round-trip tests pinning escape behaviour).

Implemented #6 → commit f43f5f0, tag v0.4.0 (Option A: DI ObjectMapper across JsonErrorHandler/AuthInterceptor/Hub/Listener; new dto/InternalDto.hpp with 4 message DTOs; 4 round-trip tests pinning escape behaviour).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: uwe.admin/oatpp-authkit#6
No description provided.