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';
}
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"
}
]
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
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
},
...
]
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
}
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
}
]
}
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
}
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"
}
]
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
}
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
}
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
}
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);
}
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');
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');
}
}
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');
}
}
Top comments (0)