SurveyJS Server Integration · PHP / Laravel IV.1 · Validate the response against the definition
You work in a private copy of the demo database, kept until it has been unused for 24 hours.

IV.1

Validate the response against the definition

You need this: When clients aren't trusted: public forms, APIs, compliance.

You need Section IV only if you can't trust the client: the browser already validates everything. Load the definition into a survey model on the server and set the submitted data; it reports answers that don't fit the definition: wrong types, unknown choices, missing required answers. Reject the save with the errors. Here the SurveyJS service does it, next to this app (docker compose up -d surveyjs).

This step on the Server Integration page Source on GitHub

The SurveyJS service is called at http://surveyjs:3000. If it isn't running, the endpoint answers 503 "SurveyJS service is not running": start it with docker compose up -d surveyjs.

Try this

  1. Fill in the claim and press Complete: POST /api/responses answers 201 after the service validated it.
  2. Send tampered responses straight to the endpoint, bypassing the form: . Each answers 400 with the errors, and nothing is stored.

Code that just ran

These regions are read from the files that served this page, the same lines the Server Integration page shows.

Client · shared/client/validate-response.js · sjs:IV.1.client View on GitHub
// Show the server's verdict in the completion message
survey.onComplete.add(async (sender, options) => {
  options.showSaveInProgress();
  const res = await fetch("/api/responses", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ formId: "claim", data: sender.data })
  });
  if (res.ok) return options.showSaveSuccess();
  const { errors, error } = await res.json();
  options.showSaveError(errors ? errors.map(describe).join("; ") : error);
});
Server · routes/examples/validate-response.php · sjs:IV.1.server View on GitHub
// POST /api/responses — ask the SurveyJS service to validate, then store as usual
Route::post('/api/responses', function (Request $request) {
    $body = json_decode($request->getContent(), flags: JSON_THROW_ON_ERROR);    // the raw body keeps {} as {} (see I.1)
    $definition = DB::table('forms')->where('key', $body->formId ?? null)->value('json') ?? abort(404, 'Unknown form');
    try {
        $check = Http::baseUrl(config('surveyjs.service_url'))->timeout(10)
            ->post('/response', ['schema' => json_decode($definition), 'response' => $body->data ?? null]);
    } catch (ConnectionException $e) {                                         // cURL error 28 is a timeout
        return str_contains($e->getMessage(), 'cURL error 28')
            ? response()->json(['error' => 'SurveyJS service timed out'], 504)
            : response()->json(['error' => 'SurveyJS service is not running at '.config('surveyjs.service_url')], 503);
    }
    if ($check->status() === 422 && $check->json('errors') !== null && ! array_key_exists('warnings', $check->json())) {
        return response()->json(['errors' => $check->object()->errors], 400);    // the answers don't fit the definition
    }
    if ($check->status() !== 200 || $check->body() !== '{}') {                  // fail closed: only exactly {} means valid
        Log::error('SurveyJS service: unexpected answer from /response', ['status' => $check->status(), 'body' => $check->body()]);
        abort(502, $check->status() === 422 && $check->json('warnings') !== null ? 'The stored definition has errors' : 'SurveyJS service error');
    }
    $id = DB::table('responses')->insertGetId(['form_id' => $body->formId, 'created_at' => now('UTC')->toIso8601ZuluString(),
        'data' => json_encode($body->data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION | JSON_THROW_ON_ERROR)]);

    return response()->json(['id' => $id], 201);
});

Definition: shared/definitions/relational-storage.v1.json