DEV Community

Cover image for laravel: powerful json with jsonSerialize()
grant horwood
grant horwood

Posted on • Updated on

laravel: powerful json with jsonSerialize()

laravel does a pretty good job of transparently translating models to json so long as you keep things simple. however, that ease of use breaks down pretty quickly once you decide to make meaninful edits to your json representations. if you've ever found yourself fighting with hidden vs. visible, or fiddling with casts or accessors, you probably understand.

fortunately, laravel provides an easy way to define a model's json serialization in one method: jsonSerialize().

why use jsonSerialize?

although not the 'canonical' way to do serialization, jsonSerialize() provides both power and improved readability.

by using jsonSerialize() we can add, delete or modify any field in our model without having to resort to mixing various configuration values. and because our json definition is all in one method, our code is a bit more readable.

the flyover

we can do a lot with jsonSerialize, but this article is only going to cover the basics, namely:

  • a simple implementation to modify json structure and returned data
  • including a relationship in our json
  • subclassing models to create new serialization schemas

the data structure

since we're working with models here, we're going to do a fast explanation of the project and data structure for the examples that follow.

our project is a simple api for viewing a vinyl record collection. the data structure consists of records, with each record having one more more songs.

the api has three endpoints:

  • GET http://api.bintracker.test/api/songs to return all the songs in the database
  • GET http://api.bintracker.test/api/records to return a digest of all the records
  • GET http://api.bintracker.test/api/records/{id} to return details on one record, including all of that record's songs.

the migrations for our tables are shown at the bottom of the article.

we will be going over the models and the controllers in the rest of the article.

a simple json format with jsonSerialize

the first model we're going to look at is Song.php.

before adding jsonSerialize, the model looks like this:

app/Models/Song.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Song extends Model
{
    use HasFactory;

    protected $table = 'songs';
}
Enter fullscreen mode Exit fullscreen mode

when we call our GET /api/songs endpoint to return all songs in our db, the result looks like:

[
  {
    "id": 1,
    "record_id": 1,
    "title": "Bait and Switch",
    "duration_seconds": 148,
    "created_at": "2022-05-02T22:32:01.000000Z",
    "updated_at": "2022-05-02T22:32:01.000000Z"
  },
  {
    "id": 2,
    "record_id": 1,
    "title": "Sneaking into yer House",
    "duration_seconds": 80,
    "created_at": "2022-05-02T22:32:14.000000Z",
    "updated_at": "2022-05-02T22:32:14.000000Z"
  },
  {
    "id": 3,
    "record_id": 1,
    "title": "Dippity Do-Nut",
    "duration_seconds": 112,
    "created_at": "2022-05-03T11:16:25.000000Z",
    "updated_at": "2022-05-03T11:16:25.000000Z"
  }
]
Enter fullscreen mode Exit fullscreen mode

that's not great output. let's fix it with jsonSerialize()!

at the bottom of our Song model, we'll add this method:

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            // cast seconds to minutes and seconds
            'duration' => gmdate("i:s", $this->duration_seconds),
            // a new value
            'long_enough_for_radio_play' => $this->duration_seconds > 120 ? true: false,
        ];
    } // jsonSerialize
Enter fullscreen mode Exit fullscreen mode

let's take a look at what's going on here.

first we declare the public method jsonSerialize(). this method is called every time this model is serialized to json for both single objects and collections.

we notice that it returns an associative array. this array is what defines our json. in the example above, we have explicitly included id and title but have left out both the record timestamps. the result is that id and title show up in our json but, for instance, created_at does not.

we can also see that we have applied a transformation to the duration_seconds column to convert seconds to minutes and seconds, assigning it to the key duration, and have added a new key/value of long_enough_for_radio_play to indicate if the song is more than two minutes long.

this is one of the more powerful aspects of jsonSerialize(): we can apply casts and transformations to any column we want, rename keys, and even inject new key/value pairs.

when we call GET /api/songs again, our json output now looks like:

[
  {
    "id": 1,
    "title": "Bait and Switch",
    "duration": "02:28",
    "long_enough_for_radio_play": true
  },
...
]
Enter fullscreen mode Exit fullscreen mode

our new keys are there and the timestamps are gone!

composite objects with jsonSerialize

so far we've looked at serializing simple objects. now, let's move on to composite objects: objects that contain other objects.

let's look at the model for Record:

app/Models/Record.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Record extends Model
{
    use HasFactory;

    protected $table = 'records';

    /**
     * Returns all songs for record
     *
     * @return HasMany
     */
    protected function songs():HasMany
    {
        return $this->hasMany('App\Models\Song', 'record_id', 'id')->orderBy('id', 'asc');
    }

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'artist' => $this->artist,
            'title' => $this->title,
            'songs' => $this->songs, // array of songs
        ];
    } // jsonSerialize
}
Enter fullscreen mode Exit fullscreen mode

since records contain songs, we want our Record model to include an array of that record's Song models. doing this requires two things.

first, we write the songs() function. this function creates a hasMany() relationship to songs and returns it. if you've done a lot of eloquent before, this should be familiar.

then, in our jsonSerialize() method, we include those songs by calling the songs() method. the result is an array of songs keyed as songs.

we should note that the json format of all those songs is defined by the Song model's own jsonSerialize() method. very handy!

once we have this written, our endpoint GET /api/records/{id}, which returns Record::all(), will give us output that looks like this:

{
  "id": 1,
  "artist": "Emily's Sassy Lime",
  "title": "Dippity Do-Nut",
  "songs": [
    {
      "id": 1,
      "title": "Bait and Switch",
      "duration": "02:28",
      "long_enough_for_radio_play": true
    },
    {
      "id": 2,
      "title": "Sneaking into yer House",
      "duration": "01:20",
      "long_enough_for_radio_play": false
    },
    {
      "id": 3,
      "title": "Dippity Do-Nut",
      "duration": "01:52",
      "long_enough_for_radio_play": false
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

subclassing for different formats

sometimes, we want two different json schemas for the same model. for instance, if we wanted to write an endpoint that returned a list of all our records, we'd probably want a array of smaller objects; a 'digest'. maybe we just want the artist, title and id fields.

we can do this by subclassing our Record model and overloading jsonSerialize. by making our new model, let's call it RecordDigest, a subclass of Record, we can access all of the methods and data in the superclass if we want, but our default json serialization will be our new one. let's look at the RecordDigest model.

app/Models/RecordDigest.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class RecordDigest extends Record
{

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'artist' => $this->artist,
            'title' => $this->title,
        ];
    } // jsonSerialize
}
Enter fullscreen mode Exit fullscreen mode

this is a whole new model, but because it inherits from Record, it still accesses the records table and we still have access to the songs() method if we wish to use it.

if we write an endpoint GET /api/records that retuns RecordDigest::all(), we will see our json output looks like:

[
  {
    "id": 1,
    "artist": "Emily's Sassy Lime",
    "title": "Dippity Do-Nut"
  },
  {
    "id": 2,
    "artist": "Bratmobile",
    "title": "Pottymouth"
  }
]
Enter fullscreen mode Exit fullscreen mode

exactly what we wanted.

conclusion

there's a lot we can do with jsonSerialize(). i often use it as a way to inject hateoas data into my returns, or to tailor json data or structure depending on, say, the access role of the user. by creating these formalized ways to define our json we get not just power, but also consistency and improved readability.

supplemental code

if you want to inspect this example more, the source is listed here:

models

Song.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Song extends Model
{
    use HasFactory;

    protected $table = 'songs';

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            // cast seconds to minutes and seconds
            'duration' => gmdate("i:s", $this->duration_seconds),
        ];
    } // jsonSerialize
}
Enter fullscreen mode Exit fullscreen mode

Record.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Record extends Model
{
    use HasFactory;

    protected $table = 'records';

    /**
     * Returns all songs for record
     *
     * @return HasMany
     */
    protected function songs():HasMany
    {
        return $this->hasMany('App\Models\Song', 'record_id', 'id')->orderBy('id', 'desc');
    }

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'artist' => $this->artist,
            'title' => $this->title,
            'songs' => $this->songs, // array of songs
        ];
    } // jsonSerialize
}
Enter fullscreen mode Exit fullscreen mode

RecordDigest.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class RecordDigest extends Record
{

    /**
     * Define the json format of the model
     */
    public function jsonSerialize():Array
    {
        return [
            'id' => $this->id,
            'artist' => $this->artist,
            'title' => $this->title,
        ];
    } // jsonSerialize
}
Enter fullscreen mode Exit fullscreen mode

controller

RecordController.php

<?php

namespace App\Http\Controllers\api;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;

use App\Models\Song;
use App\Models\Record;
use App\Models\RecordDigest;

class RecordController extends Controller
{
    /**
     * Get all songs
     *
     * @return JsonResponse
     */
    public function getAllSongs(Request $request):JsonResponse
    {
        $songs = Song::all();

        return response()->json($songs, 200);
    }

    /**
     * Get one record
     *
     * @return JsonResponse
     */
    public function getRecord(Int $id, Request $request):JsonResponse
    {
        $records = Record::find($id);

        return response()->json($records, 200);
    }

    /**
     * Get all records as digests
     *
     * @return JsonResponse
     */
    public function getAllRecords(Request $request):JsonResponse
    {
        $records = RecordDigest::all();

        return response()->json($records, 200);
    }
Enter fullscreen mode Exit fullscreen mode

routes

routes/api.php

Route::get('songs', '\App\Http\Controllers\api\RecordController@getAllSongs');
Route::get('records', '\App\Http\Controllers\api\RecordController@getAllRecords');
Route::get('records/{id}', '\App\Http\Controllers\api\RecordController@getRecord');
Enter fullscreen mode Exit fullscreen mode

migrations

records migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRecordsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('records', function (Blueprint $table) {
            $table->id();
            $table->string('artist');
            $table->string('title');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('records');
    }
}
Enter fullscreen mode Exit fullscreen mode

songs migration

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateSongsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('songs', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('record_id');
            $table->foreign('record_id')->references('id')->on('records');
            $table->string("title");
            $table->integer("duration_seconds");
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('songs');
    }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)